diff --git a/.doctor-rst.yaml b/.doctor-rst.yaml index efc63dba577..2d698b77c20 100644 --- a/.doctor-rst.yaml +++ b/.doctor-rst.yaml @@ -36,13 +36,22 @@ rules: valid_inline_highlighted_namespaces: ~ indention: ~ unused_links: ~ + yaml_instead_of_yml_suffix: ~ + extend_abstract_controller: ~ +# no_app_bundle: ~ - # 3.4 + # master versionadded_directive_major_version: - major_version: 3 + major_version: 5 versionadded_directive_min_version: - min_version: '3.0' + min_version: '5.0' + + deprecated_directive_major_version: + major_version: 5 + + deprecated_directive_min_version: + min_version: '5.0' # do not report as violation whitelist: @@ -66,8 +75,13 @@ whitelist: - '.. versionadded:: 1.11' # MakerBundle - '.. versionadded:: 1.3' # MakerBundle - '.. versionadded:: 1.8' # MakerBundle + - '.. versionadded:: 1.6' # Flex in setup/upgrade_minor.rst - '0 => 123' # assertion for var_dumper - components/var_dumper.rst - '1 => "foo"' # assertion for var_dumper - components/var_dumper.rst + - '123,' # assertion for var_dumper - components/var_dumper.rst + - '"foo",' # assertion for var_dumper - components/var_dumper.rst - '$var .= "Because of this `\xE9` octet (\\xE9),\n";' - "`Deploying Symfony 4 Apps on Heroku`_." - ".. _`Deploying Symfony 4 Apps on Heroku`: https://devcenter.heroku.com/articles/deploying-symfony4" + - "// 224, 165, 141, 224, 164, 164, 224, 165, 135])" + - '.. versionadded:: 0.2' # MercureBundle diff --git a/_build/redirection_map b/_build/redirection_map index ccb41e8ada9..5d44f67b39d 100644 --- a/_build/redirection_map +++ b/_build/redirection_map @@ -77,14 +77,15 @@ /book/configuration /configuration /book/propel /propel/propel /book/performance /performance +/bundles/installation /bundles /cookbook/assetic/apply_to_option /frontend/assetic/apply_to_option /cookbook/assetic/asset_management /frontend/assetic/asset_management -/cookbook/assetic/index /frontend/assetic +/cookbook/assetic/index /frontend/assetic/index /cookbook/assetic/jpeg_optimize /frontend/assetic/jpeg_optimize /cookbook/assetic/php /frontend/assetic/php /cookbook/assetic/uglifyjs /frontend/assetic/uglifyjs /cookbook/assetic/yuicompressor /frontend/assetic/yuicompressor -/assetic /frontend/assetic +/assetic /frontend/assetic/index /assetic/apply_to_option /frontend/assetic/apply_to_option /assetic/asset_management /frontend/assetic/asset_management /assetic/jpeg_optimize /frontend/assetic/jpeg_optimize @@ -96,10 +97,11 @@ /cookbook/bundles/extension /bundles/extension /cookbook/bundles/index /bundles /cookbook/bundles/inheritance /bundles/inheritance -/cookbook/bundles/installation /bundles/installation +/cookbook/bundles/installation /bundles /cookbook/bundles/override /bundles/override /cookbook/bundles/prepend_extension /bundles/prepend_extension -/cookbook/bundles/remove /bundles/remove +/cookbook/bundles/remove /bundles +/bundles/remove /bundles /cookbook/cache/form_csrf_caching /http_cache/form_csrf_caching /cookbook/cache/varnish /http_cache/varnish /cookbook/composer /setup/composer @@ -120,14 +122,16 @@ /cookbook/console/logging /console /cookbook/console/request_context /console/request_context /cookbook/console/style /console/style -/cookbook/console/usage /console/usage -/cookbook/controller/csrf_token_validation /controller/csrf_token_validation +/cookbook/console/usage /console +/console/usage /console +/cookbook/controller/csrf_token_validation /security/csrf /cookbook/controller/error_pages /controller/error_pages /cookbook/controller/forwarding /controller/forwarding /cookbook/controller/index /controller /cookbook/controller/service /controller/service /cookbook/controller/upload_file /controller/upload_file -/cookbook/debugging /debug/debugging +/cookbook/debugging / +/debug/debugging / /cookbook/deployment/azure-website /cookbook/azure-website /cookbook/deployment/fortrabbit /deployment/fortrabbit /cookbook/deployment/heroku /deployment/heroku @@ -135,22 +139,25 @@ /cookbook/deployment/platformsh /deployment/platformsh /cookbook/deployment/tools /deployment/tools /cookbook/doctrine/common_extensions /doctrine/common_extensions -/cookbook/doctrine/console /doctrine/console +/cookbook/doctrine/console /doctrine /cookbook/doctrine/custom_dql_functions /doctrine/custom_dql_functions /cookbook/doctrine/dbal /doctrine/dbal /cookbook/doctrine/event_listeners_subscribers /doctrine/event_listeners_subscribers /cookbook/doctrine/index /doctrine -/cookbook/doctrine/mapping_model_classes /doctrine/mapping_model_classes +/cookbook/doctrine/mapping_model_classes /doctrine +/doctrine/mapping_model_classes /doctrine /cookbook/doctrine/mongodb_session_storage /doctrine/mongodb_session_storage /cookbook/doctrine/multiple_entity_managers /doctrine/multiple_entity_managers /cookbook/doctrine/pdo_session_storage /doctrine/pdo_session_storage /cookbook/doctrine/registration_form /doctrine/registration_form /cookbook/doctrine/resolve_target_entity /doctrine/resolve_target_entity /cookbook/doctrine/reverse_engineering /doctrine/reverse_engineering -/cookbook/email/cloud /email/cloud +/doctrine/repository /doctrine +/doctrine/console /doctrine +/cookbook/email/cloud /email /cookbook/email/dev_environment /email/dev_environment /cookbook/email/email /email -/cookbook/email/gmail /email/gmail +/cookbook/email/gmail /email /cookbook/email/index /email /cookbook/email/spool /email/spool /cookbook/email/testing /email/testing @@ -214,7 +221,7 @@ /cookbook/security/acl /security/acl /cookbook/security/acl_advanced /security/acl_advanced /cookbook/security/api_key_authentication /security/api_key_authentication -/cookbook/security/csrf_in_login_form /security/csrf_in_login_form +/cookbook/security/csrf_in_login_form /security/csrf /cookbook/security/custom_authentication_provider /security/custom_authentication_provider /cookbook/security/custom_password_authenticator /security/custom_password_authenticator /cookbook/security/custom_provider /security/custom_provider @@ -280,18 +287,28 @@ /cookbook/web_services/php_soap_extension /controller/soap_web_service /cookbook/workflow/homestead /setup/homestead /cookbook/workflow/index /setup -/cookbook/workflow/new_project_git /setup/new_project_git -/cookbook/workflow/new_project_svn /setup/new_project_svn +/cookbook/workflow/new_project_git /setup +/cookbook/workflow/new_project_svn /setup +/setup/new_project_git /setup +/setup/new_project_svn /setup /components/asset/index /components/asset /components/asset/introduction /components/asset /components/browser_kit/index /components/browser_kit /components/browser_kit/introduction /components/browser_kit /components/class_loader/introduction /components/class_loader /components/class_loader/index /components/class_loader +/components/class_loader/cache_class_loader /components/class_loader +/components/class_loader/class_loader /components/class_loader +/components/class_loader/class_map_generator /components/class_loader +/components/class_loader/debug_class_loader /components/class_loader +/components/class_loader/map_class_loader /components/class_loader +/components/class_loader/map_class_loader /components/class_loader +/components/class_loader/psr4_class_loader /components/class_loader /components/config/introduction /components/config /components/config/index /components/config /components/console/helpers/tablehelper /components/console/helpers/table /components/console/helpers/progresshelper /components/console/helpers/progressbar +/components/console/helpers/dialoghelper /components/console/helpers/questionhelper /components/console/introduction /components/console /components/console/index /components/console /components/debug/class_loader /components/debug @@ -343,31 +360,120 @@ /components/var_dumper/index /components/var_dumper /components/yaml/introduction /components/yaml /components/yaml/index /components/yaml +/console/logging /console +/controller/csrf_token_validation /security/csrf /deployment/tools /deployment +/form/csrf_protection /security/csrf /install/bundles /setup/bundles +/email/gmail /email +/email/cloud /email /event_dispatcher/class_extension /event_dispatcher /form /forms /form/use_virtual_forms /form/inherit_data_option +/frontend/assetic /frontend/assetic/index +/frontend/assetic/apply_to_option /frontend/assetic/index +/frontend/assetic/asset_management /frontend/assetic/index +/frontend/assetic/jpeg_optimize /frontend/assetic/index +/frontend/assetic/php /frontend/assetic/index +/frontend/assetic/uglifyjs /frontend/assetic/index +/frontend/assetic/yuicompressor /frontend/assetic/index +/reference/configuration/assetic /frontend/assetic/index /security/target_path /security +/security/csrf_in_login_form /security/csrf /service_container/service_locators /service_container/service_subscribers_locators /service_container/third_party /service_container /templating/templating_service /templates /testing/simulating_authentication /testing/http_authentication /validation/group_service_resolver /form/validation_group_service_resolver /request/load_balancer_reverse_proxy /deployment/proxies +/quick_tour/the_controller /quick_tour/the_big_picture +/quick_tour/the_view /quick_tour/flex_recipes /service_container/service_locators /service_container/service_subscribers_locators +/templating/overriding /bundles/override +/security/custom_provider /security/user_provider +/security/multiple_user_providers /security/user_provider +/security/custom_password_authenticator /security/guard_authentication +/security/api_key_authentication /security/guard_authentication +/security/pre_authenticated /security/auth_providers +/security/host_restriction /security/firewall_restriction +/security/acl_advanced /security/acl +/security/password_encoding /security /weblink /web_link /components/weblink /components/web_link /frontend/encore/installation-no-flex /frontend/encore/installation +/http_cache/form_csrf_caching /security/csrf /console/logging /console +/reference/forms/twig_reference /form/form_customization +/form/rendering /form/form_customization +/profiler/matchers /profiler +/profiler/profiling_data /profiler +/profiler/wdt_follow_ajax /profiler +/security/entity_provider /security/user_provider +/session/avoid_session_start /session +/session/sessions_directory /session /frontend/encore/legacy-apps /frontend/encore/legacy-applications +/configuration/external_parameters /configuration/environment_variables /contributing/code/patches /contributing/code/pull_requests /workflow/state-machines /workflow/workflow-and-state-machine /workflow/introduction /workflow/workflow-and-state-machine /workflow/usage /workflow /introduction/from_flat_php_to_symfony2 /introduction/from_flat_php_to_symfony +/configuration/environment_variables /configuration/env_var_processors +/configuration/configuration_organization /configuration +/configuration/environments /configuration +/configuration/configuration_organization /configuration +/email/dev_environment /mailer +/email/spool /mailer +/email/testing /mailer /contributing/community/other /contributing/community +/profiler/storage /profiler /setup/composer /setup +/security/security_checker /setup +/setup/built_in_web_server /setup/symfony_server +/service_container/parameters /configuration +/routing/generate_url_javascript /routing +/routing/slash_in_parameter /routing +/routing/scheme /routing +/routing/optional_placeholders /routing +/routing/conditions /routing +/routing/requirements /routing +/routing/redirect_trailing_slash /routing +/routing/debug /routing +/routing/service_container_parameters /routing +/routing/redirect_in_config /routing +/routing/external_resources /routing +/routing/hostname_pattern /routing +/routing/extra_information /routing +/console/request_context /routing +/form/action_method /forms +/reference/requirements /setup +/bundles/inheritance /bundles/override +/templating /templates +/templating/escaping /templates +/templating/syntax /templates +/templating/debug /templates +/templating/render_without_controller /templates +/templating/app_variable /templates +/templating/formats /templates +/templating/namespaced_paths /templates +/templating/embedding_controllers /templates +/templating/inheritance /templates +/testing/doctrine /testing/database +/doctrine/lifecycle_callbacks /doctrine/events +/doctrine/event_listeners_subscribers /doctrine/events +/doctrine/common_extensions /doctrine +/best_practices/index /best_practices +/best_practices/introduction /best_practices +/best_practices/creating-the-project /best_practices +/best_practices/configuration /best_practices +/best_practices/business-logic /best_practices +/best_practices/controllers /best_practices +/best_practices/templates /best_practices +/best_practices/forms /best_practices +/best_practices/i18n /best_practices +/best_practices/security /best_practices +/best_practices/web-assets /best_practices +/best_practices/tests /best_practices /components/debug https://github.com/symfony/debug /components/translation https://github.com/symfony/translation /components/translation/usage /translation diff --git a/_images/components/messenger/overview.svg b/_images/components/messenger/overview.svg new file mode 100644 index 00000000000..94737e7a6da --- /dev/null +++ b/_images/components/messenger/overview.svg @@ -0,0 +1 @@ + diff --git a/_images/components/serializer/serializer_workflow.png b/_images/components/serializer/serializer_workflow.png deleted file mode 100644 index 3e1944e6cec..00000000000 Binary files a/_images/components/serializer/serializer_workflow.png and /dev/null differ diff --git a/_images/components/serializer/serializer_workflow.svg b/_images/components/serializer/serializer_workflow.svg new file mode 100644 index 00000000000..f3906506878 --- /dev/null +++ b/_images/components/serializer/serializer_workflow.svg @@ -0,0 +1 @@ + diff --git a/_images/components/string/bytes-points-graphemes.png b/_images/components/string/bytes-points-graphemes.png new file mode 100644 index 00000000000..18d971cecf7 Binary files /dev/null and b/_images/components/string/bytes-points-graphemes.png differ diff --git a/_images/components/workflow/blogpost_puml.png b/_images/components/workflow/blogpost_puml.png new file mode 100644 index 00000000000..14d45c8b40f Binary files /dev/null and b/_images/components/workflow/blogpost_puml.png differ diff --git a/_images/components/workflow/pull_request.png b/_images/components/workflow/pull_request.png index 1aa8886728c..692a95345ae 100644 Binary files a/_images/components/workflow/pull_request.png and b/_images/components/workflow/pull_request.png differ diff --git a/_images/components/workflow/pull_request_puml_styled.png b/_images/components/workflow/pull_request_puml_styled.png new file mode 100644 index 00000000000..cda9233d731 Binary files /dev/null and b/_images/components/workflow/pull_request_puml_styled.png differ diff --git a/_images/controller/error_pages/errors-in-prod-environment.png b/_images/controller/error_pages/errors-in-prod-environment.png index 79fe5341b47..808d0d70028 100644 Binary files a/_images/controller/error_pages/errors-in-prod-environment.png and b/_images/controller/error_pages/errors-in-prod-environment.png differ diff --git a/_images/controller/error_pages/exceptions-in-dev-environment.png b/_images/controller/error_pages/exceptions-in-dev-environment.png index 5e7da2cf6a1..74128990e57 100644 Binary files a/_images/controller/error_pages/exceptions-in-dev-environment.png and b/_images/controller/error_pages/exceptions-in-dev-environment.png differ diff --git a/_images/form/form-custom-type-postal-address-fragment-names.svg b/_images/form/form-custom-type-postal-address-fragment-names.svg new file mode 100644 index 00000000000..9b6092c9808 --- /dev/null +++ b/_images/form/form-custom-type-postal-address-fragment-names.svg @@ -0,0 +1 @@ + diff --git a/_images/form/form-custom-type-postal-address.svg b/_images/form/form-custom-type-postal-address.svg new file mode 100644 index 00000000000..ab0fde8af3a --- /dev/null +++ b/_images/form/form-custom-type-postal-address.svg @@ -0,0 +1 @@ + diff --git a/_images/form/form-field-parts.svg b/_images/form/form-field-parts.svg new file mode 100644 index 00000000000..c9856c89a99 --- /dev/null +++ b/_images/form/form-field-parts.svg @@ -0,0 +1 @@ + diff --git a/_images/http/request-flow.png b/_images/http/request-flow.png deleted file mode 100644 index cbf4019307b..00000000000 Binary files a/_images/http/request-flow.png and /dev/null differ diff --git a/_images/http/request-flow.svg b/_images/http/request-flow.svg new file mode 100644 index 00000000000..97061ada0d5 --- /dev/null +++ b/_images/http/request-flow.svg @@ -0,0 +1 @@ + diff --git a/_images/mercure/chrome.png b/_images/mercure/chrome.png new file mode 100644 index 00000000000..8ccc55a0a88 Binary files /dev/null and b/_images/mercure/chrome.png differ diff --git a/_images/mercure/discovery.png b/_images/mercure/discovery.png new file mode 100644 index 00000000000..0ef38271de6 Binary files /dev/null and b/_images/mercure/discovery.png differ diff --git a/_images/mercure/panel.png b/_images/mercure/panel.png new file mode 100644 index 00000000000..22b214f5ff2 Binary files /dev/null and b/_images/mercure/panel.png differ diff --git a/_images/mercure/schema.png b/_images/mercure/schema.png new file mode 100644 index 00000000000..4616046e5cc Binary files /dev/null and b/_images/mercure/schema.png differ diff --git a/_images/profiler/web-interface.png b/_images/profiler/web-interface.png new file mode 100644 index 00000000000..2e6c6061892 Binary files /dev/null and b/_images/profiler/web-interface.png differ diff --git a/_images/quick_tour/no_routes_page.png b/_images/quick_tour/no_routes_page.png new file mode 100644 index 00000000000..382950b6ef5 Binary files /dev/null and b/_images/quick_tour/no_routes_page.png differ diff --git a/_images/quick_tour/profiler.png b/_images/quick_tour/profiler.png deleted file mode 100644 index 3b55b75f3af..00000000000 Binary files a/_images/quick_tour/profiler.png and /dev/null differ diff --git a/_images/quick_tour/web_debug_toolbar.png b/_images/quick_tour/web_debug_toolbar.png index 72cd7483f2f..465020380cb 100644 Binary files a/_images/quick_tour/web_debug_toolbar.png and b/_images/quick_tour/web_debug_toolbar.png differ diff --git a/_images/quick_tour/welcome.png b/_images/quick_tour/welcome.png deleted file mode 100644 index 738105f715d..00000000000 Binary files a/_images/quick_tour/welcome.png and /dev/null differ diff --git a/_images/security/authentication-guard-methods.svg b/_images/security/authentication-guard-methods.svg index a18da9e66dc..cc042656212 100644 --- a/_images/security/authentication-guard-methods.svg +++ b/_images/security/authentication-guard-methods.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/_images/security/http_basic_popup.png b/_images/security/http_basic_popup.png deleted file mode 100644 index fcd9a4ed836..00000000000 Binary files a/_images/security/http_basic_popup.png and /dev/null differ diff --git a/_images/security/symfony_loggedin_wdt.png b/_images/security/symfony_loggedin_wdt.png index ca182192c94..b51e1cafba1 100644 Binary files a/_images/security/symfony_loggedin_wdt.png and b/_images/security/symfony_loggedin_wdt.png differ diff --git a/_images/sources/components/messenger/overview.dia b/_images/sources/components/messenger/overview.dia new file mode 100644 index 00000000000..55ee153439e Binary files /dev/null and b/_images/sources/components/messenger/overview.dia differ diff --git a/_images/sources/components/serializer/serializer_workflow.dia b/_images/sources/components/serializer/serializer_workflow.dia new file mode 100644 index 00000000000..6cb44280d0d Binary files /dev/null and b/_images/sources/components/serializer/serializer_workflow.dia differ diff --git a/_images/sources/form/form-custom-type-postal-address-fragment-names.dia b/_images/sources/form/form-custom-type-postal-address-fragment-names.dia new file mode 100644 index 00000000000..aebdadb4170 Binary files /dev/null and b/_images/sources/form/form-custom-type-postal-address-fragment-names.dia differ diff --git a/_images/sources/form/form-custom-type-postal-address.dia b/_images/sources/form/form-custom-type-postal-address.dia new file mode 100644 index 00000000000..35a1eaebfd6 Binary files /dev/null and b/_images/sources/form/form-custom-type-postal-address.dia differ diff --git a/_images/sources/form/form-field-parts.dia b/_images/sources/form/form-field-parts.dia new file mode 100644 index 00000000000..d6ed2dfc3fe Binary files /dev/null and b/_images/sources/form/form-field-parts.dia differ diff --git a/_images/sources/http/request-flow.dia b/_images/sources/http/request-flow.dia new file mode 100644 index 00000000000..ca09a05504e Binary files /dev/null and b/_images/sources/http/request-flow.dia differ diff --git a/_images/sources/security/authentication-guard-methods.dia b/_images/sources/security/authentication-guard-methods.dia index 283e74b2e05..d655be780fe 100644 Binary files a/_images/sources/security/authentication-guard-methods.dia and b/_images/sources/security/authentication-guard-methods.dia differ diff --git a/_includes/service_container/_my_mailer.rst.inc b/_includes/service_container/_my_mailer.rst.inc index a944097f917..01eafdfe87a 100644 --- a/_includes/service_container/_my_mailer.rst.inc +++ b/_includes/service_container/_my_mailer.rst.inc @@ -2,15 +2,15 @@ .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: app.mailer: - class: AppBundle\Mailer + class: App\Mailer arguments: [sendmail] .. code-block:: xml - + - + sendmail @@ -26,8 +26,8 @@ .. code-block:: php - // app/config/services.php - use AppBundle\Mailer; + // config/services.php + use App\Mailer; $container->register('app.mailer', Mailer::class) ->addArgument('sendmail'); diff --git a/assetic/_standard_edition_warning.rst.inc b/assetic/_standard_edition_warning.rst.inc deleted file mode 100644 index 2a111cd2291..00000000000 --- a/assetic/_standard_edition_warning.rst.inc +++ /dev/null @@ -1,5 +0,0 @@ -.. caution:: - - Starting from Symfony 2.8, Assetic is no longer included by default in the - Symfony Standard Edition. Refer to :doc:`this article ` - to learn how to install and enable Assetic in your Symfony application. diff --git a/best_practices.rst b/best_practices.rst new file mode 100644 index 00000000000..5616f1f8120 --- /dev/null +++ b/best_practices.rst @@ -0,0 +1,450 @@ +The Symfony Framework Best Practices +==================================== + +This article describes the **best practices for developing web applications with +Symfony** that fit the philosophy envisioned by the original Symfony creators. + +If you don't agree with some of these recommendations, they might be a good +**starting point** that you can then **extend and fit to your specific needs**. +You can even ignore them completely and continue using your own best practices +and methodologies. Symfony is flexible enough to adapt to your needs. + +This article assumes that you already have experience developing Symfony +applications. If you don't, read first the rest of the `Symfony documentation`_. + +.. tip:: + + Symfony provides a sample application called `Symfony Demo`_ that follows + all these best practices, so you can experience them in practice. + +Creating the Project +-------------------- + +Use the Symfony Binary to Create Symfony Applications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Symfony binary is an executable command created in your machine when you +`download Symfony`_. It provides multiple utilities, including the simplest way +to create new Symfony applications: + +.. code-block:: terminal + + $ symfony new my_project_name + +Under the hood, this Symfony binary command executes the needed `Composer`_ +command to :ref:`create a new Symfony application ` +based on the current stable version. + +Use the Default Directory Structure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Unless your project follows a development practice that imposes a certain +directory structure, follow the default Symfony directory structure. It's flat, +self-explanatory and not coupled to Symfony: + +.. code-block:: text + + your_project/ + ├─ assets/ + ├─ bin/ + │ └─ console + ├─ config/ + │ ├─ packages/ + │ └─ services.yaml + └─ public/ + │ ├─ build/ + │ └─ index.php + ├─ src/ + │ ├─ Kernel.php + │ ├─ Command/ + │ ├─ Controller/ + │ ├─ DataFixtures/ + │ ├─ Entity/ + │ ├─ EventSubscriber/ + │ ├─ Form/ + │ ├─ Migrations/ + │ ├─ Repository/ + │ ├─ Security/ + │ └─ Twig/ + ├─ templates/ + ├─ tests/ + ├─ translations/ + ├─ var/ + │ ├─ cache/ + │ └─ log/ + └─ vendor/ + +Configuration +------------- + +Use Environment Variables for Infrastructure Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These are the options that change from one machine to another (e.g. from your +development machine to the production server) but which don't change the +application behavior. + +:ref:`Use env vars in your project ` to define these options +and create multiple ``.env`` files to :ref:`configure env vars per environment `. + +Use Secret for Sensitive Information +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When your application has sensitive configuration - like an API key - you should +store those securely via :doc:`secrets `. + +Use Parameters for Application Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These are the options used to modify the application behavior, such as the sender +of email notifications, or the enabled `feature toggles`_. Their value doesn't +change per machine, so don't define them as environment variables. + +Define these options as :ref:`parameters ` in the +``config/services.yaml`` file. You can override these options per +:ref:`environment ` in the ``config/services_dev.yaml`` +and ``config/services_prod.yaml`` files. + +Use Short and Prefixed Parameter Names +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Consider using ``app.`` as the prefix of your :ref:`parameters ` +to avoid collisions with Symfony and third-party bundles/libraries parameters. +Then, use just one or two words to describe the purpose of the parameter: + +.. code-block:: yaml + + # config/services.yaml + parameters: + # don't do this: 'dir' is too generic and it doesn't convey any meaning + app.dir: '...' + # do this: short but easy to understand names + app.contents_dir: '...' + # it's OK to use dots, underscores, dashes or nothing, but always + # be consistent and use the same format for all the parameters + app.dir.contents: '...' + app.contents-dir: '...' + +Use Constants to Define Options that Rarely Change +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Configuration options like the number of items to display in some listing rarely +change. Instead of defining them as :ref:`service container parameters `, +define them as PHP constants in the related classes. Example:: + + // src/Entity/Post.php + namespace App\Entity; + + class Post + { + public const NUMBER_OF_ITEMS = 10; + + // ... + } + +The main advantage of constants is that you can use them everywhere, including +Twig templates and Doctrine entities, whereas parameters are only available +from places with access to the :doc:`service container `. + +The only notable disadvantage of using constants for this kind of configuration +values is that it's complicated to redefine their values in your tests. + +Business Logic +-------------- + +Don't Create any Bundle to Organize your Application Logic +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When Symfony 2.0 was released, applications used :doc:`bundles ` to +divide their code into logical features: UserBundle, ProductBundle, +InvoiceBundle, etc. However, a bundle is meant to be something that can be +reused as a stand-alone piece of software. + +If you need to reuse some feature in your projects, create a bundle for it (in a +private repository, to not make it publicly available). For the rest of your +application code, use PHP namespaces to organize code instead of bundles. + +Use Autowiring to Automate the Configuration of Application Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:doc:`Service autowiring ` is a feature that +reads the type-hints on your constructor (or other methods) and automatically +passes the correct services to each method, making unnecessary to configure +services explicitly and simplifying the application maintenance. + +Use it in combination with :ref:`service autoconfiguration ` +to also add :doc:`service tags ` to the services +needing them, such as Twig extensions, event subscribers, etc. + +Services Should be Private Whenever Possible +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:ref:`Make services private ` to prevent you from accessing +those services via ``$container->get()``. Instead, you will need to use proper +dependency injection. + +Use the YAML Format to Configure your Own Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you use the :ref:`default services.yaml configuration `, +most services will be configured automatically. However, in some edge cases +you'll need to configure services (or parts of them) manually. + +YAML is the format recommended to configure services because it's friendly to +newcomers and concise, but Symfony also supports XML and PHP configuration. + +Use Annotations to Define the Doctrine Entity Mapping +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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 several metadata formats, but it's recommended to use +annotations because they are by far the most convenient and agile way of setting +up and looking for mapping information. + +Controllers +----------- + +Make your Controller Extend the ``AbstractController`` Base Controller +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides a :ref:`base controller ` +which includes shortcuts for the most common needs such as rendering templates +or checking security permissions. + +Extending your controllers from this base controller couples your application +to Symfony. Coupling is generally wrong, but it may be OK in this case because +controllers shouldn't contain any business logic. Controllers should contain +nothing more than a few lines of *glue-code*, so you are not coupling the +important parts of your application. + +.. _best-practice-controller-annotations: + +Use Annotations to Configure Routing, Caching and Security +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using annotations for routing, caching and security simplifies configuration. +You don't need to browse several files created with different formats (YAML, XML, +PHP): all the configuration is just where you need it and it only uses one format. + +Don't Use Annotations to Configure the Controller Template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``@Template`` annotation is useful, but also involves some *magic*. +Moreover, most of the time ``@Template`` is used without any parameters, which +makes it more difficult to know which template is being rendered. It also hides +the fact that a controller should always return a ``Response`` object. + +Use Dependency Injection to Get Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you extend the base ``AbstractController``, you can only access to the most +common services (e.g ``twig``, ``router``, ``doctrine``, etc.), directly from the +container via ``$this->container->get()`` or ``$this->get()``. +Instead, you must use dependency injection to fetch services by +:ref:`type-hinting action method arguments ` or +constructor arguments. + +Use ParamConverters If They Are Convenient +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you're using :doc:`Doctrine `, then you can *optionally* use the +`ParamConverter`_ to automatically query for an entity and pass it as an argument +to your controller. It will also show a 404 page if no entity can be found. + +If the logic to get an entity from a route variable is more complex, instead of +configuring the ParamConverter, it's better to make the Doctrine query inside +the controller (e.g. by calling to a :doc:`Doctrine repository method `). + +Templates +--------- + +Use Snake Case for Template Names and Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use lowercased snake_case for template names, directories and variables (e.g. +``user_profile`` instead of ``userProfile`` and ``product/edit_form.html.twig`` +instead of ``Product/EditForm.html.twig``). + +Prefix Template Fragments with an Underscore +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Template fragments, also called *"partial templates"*, allow to +:ref:`reuse template contents `. Prefix their names +with an underscore to better differentiate them from complete templates (e.g. +``_user_metadata.html.twig`` or ``_caution_message.html.twig``). + +Forms +----- + +Define your Forms as PHP Classes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Creating :ref:`forms in classes ` allows to reuse +them in different parts of the application. Besides, not creating forms in +controllers simplify the code and maintenance of the controllers. + +Add Form Buttons in Templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Form classes should be agnostic to where they will be used. For example, the +button of a form used to both create and edit items should change from "Add new" +to "Save changes" depending on where it's used. + +Instead of adding buttons in form classes or the controllers, it's recommended +to add buttons in the templates. This also improves the separation of concerns, +because the button styling (CSS class and other attributes) is defined in the +template instead of in a PHP class. + +Define Validation Constraints on the Underlying Object +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Attaching :doc:`validation constraints ` to form fields +instead of to the mapped object prevents the validation from being reused in +other forms or other places where the object is used. + +.. _best-practice-handle-form: + +Use a Single Action to Render and Process the Form +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:ref:`Rendering forms ` and :ref:`processing forms ` +are two of the main tasks when handling forms. Both are too similar (most of the +times, almost identical), so it's much simpler to let a single controller action +handle everything. + +Internationalization +-------------------- + +Use the XLIFF Format for Your Translation Files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Of all the translation formats supported by Symfony (PHP, Qt, ``.po``, ``.mo``, +JSON, CSV, INI, etc.) XLIFF and gettext have the best 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 also supports notes in 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. + +Use Keys for Translations Instead of Content Strings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using keys simplifies the management of the translation files because you can +change the original contents in templates, controllers and services 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``. + +Security +-------- + +Define a Single Firewall +~~~~~~~~~~~~~~~~~~~~~~~~ + +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), it's +recommended to have only one firewall to keep things simple. + +Additionally, you should use the ``anonymous`` key under your firewall. If you +require users to be logged in for different sections of your site, use the +:doc:`access_control ` option. + +Use the ``auto`` Password Hasher +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :ref:`auto password hasher ` automatically +selects the best possible encoder/hasher depending on your PHP installation. +Currently, it tries to use ``sodium`` by default and falls back to ``bcrypt``. + +Use Voters to Implement Fine-grained Security Restrictions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your security logic is complex, you should create custom +:doc:`security voters ` instead of defining long expressions +inside the ``@Security`` annotation. + +Web Assets +---------- + +Use Webpack Encore to Process Web Assets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Web assets are things like CSS, JavaScript and image files that make the +frontend of your site look and work great. `Webpack`_ is the leading JavaScript +module bundler that compiles, transforms and packages assets for usage in a browser. + +:doc:`Webpack Encore ` is a JavaScript library that gets rid of most +of Webpack complexity without hiding any of its features or distorting its usage +and philosophy. It was originally created for Symfony applications, but it works +for any application using any technology. + +Tests +----- + +Smoke Test your URLs +~~~~~~~~~~~~~~~~~~~~ + +In software engineering, `smoke testing`_ consists of *"preliminary testing to +reveal simple failures severe enough to reject a prospective software release"*. +Using :ref:`PHPUnit data providers ` you can define a +functional test that checks that all application URLs load successfully:: + + // tests/ApplicationAvailabilityFunctionalTest.php + namespace App\Tests; + + use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + + class ApplicationAvailabilityFunctionalTest extends WebTestCase + { + /** + * @dataProvider urlProvider + */ + public function testPageIsSuccessful($url) + { + $client = self::createClient(); + $client->request('GET', $url); + + $this->assertResponseIsSuccessful(); + } + + public function urlProvider() + { + yield ['/']; + yield ['/posts']; + yield ['/post/fixture-post-1']; + yield ['/blog/category/fixture-category']; + yield ['/archives']; + // ... + } + } + +Add this test while creating your application because it requires little effort +and checks that none of your pages returns an error. Later you'll add more +specific tests for each page. + +Hardcode URLs in a Functional Test +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In Symfony applications it's recommended to :ref:`generate URLs ` +using routes to automatically update all links when a URL changes. However, if a +public URL changes, users won't be able to browse it unless you set up a +redirection to the new URL. + +That's why it's recommended to use raw URLs in tests instead of generating them +from routes. Whenever a route changes, tests will break and you'll know that +you must set up a redirection. + +.. _`Symfony documentation`: https://symfony.com/doc +.. _`Symfony Demo`: https://github.com/symfony/demo +.. _`download Symfony`: https://symfony.com/download +.. _`Composer`: https://getcomposer.org/ +.. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html +.. _`feature toggles`: https://en.wikipedia.org/wiki/Feature_toggle +.. _`smoke testing`: https://en.wikipedia.org/wiki/Smoke_testing_(software) +.. _`Webpack`: https://webpack.js.org/ diff --git a/best_practices/business-logic.rst b/best_practices/business-logic.rst deleted file mode 100644 index 3c329743eb0..00000000000 --- a/best_practices/business-logic.rst +++ /dev/null @@ -1,371 +0,0 @@ -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: - -.. code-block:: text - - symfony-project/ - ├─ app/ - ├─ src/ - │ └─ AppBundle/ - │ └─ Utils/ - │ └─ MyClass.php - ├─ tests/ - ├─ var/ - ├─ 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: - -.. code-block:: text - - symfony-project/ - ├─ app/ - ├─ src/ - │ ├─ Acme/ - │ │ └─ Utils/ - │ │ └─ MyClass.php - │ └─ AppBundle/ - ├─ tests/ - ├─ var/ - ├─ vendor/ - └─ web/ - -.. tip:: - - 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. - -.. code-block:: yaml - - # app/config/services.yml - services: - # ... - - # use the fully-qualified class name as the service id - AppBundle\Utils\Slugger: - public: false - -.. note:: - - If you're using the :ref:`default services.yml configuration `, - the class is auto-registered as a service. - -Traditionally, the naming convention for a service was a short, but unique -snake case key - e.g. ``app.utils.slugger``. But for most services, you should now -use the class name. - -.. best-practice:: - - The id of your application's services should be equal to their class name, - except when you have multiple services configured for the same class (in that - case, use a snake case id). - -Now you can use the custom slugger in any controller class, such as the -``AdminController``:: - - use AppBundle\Utils\Slugger; - - public function createAction(Request $request, Slugger $slugger) - { - // ... - - // you can also fetch a public service like this - // but fetching services in this way is not considered a best practice - // $slugger = $this->get('app.slugger'); - - if ($form->isSubmitted() && $form->isValid()) { - $slug = $slugger->slugify($post->getTitle()); - $post->setSlug($slug); - - // ... - } - } - -Services can also be :ref:`public or private `. If you use the -:ref:`default services.yml configuration `, -all services are private by default. - -.. best-practice:: - - Services should be ``private`` whenever possible. This will prevent you from - accessing that service via ``$container->get()``. Instead, you will need to use - dependency injection. - -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, but you can -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: - -.. code-block:: yaml - - # 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 -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. - -Doctrine support is not enabled by default in Symfony. So to use Doctrine -as shown in the examples below you will need to install :doc:`Doctrine ORM support ` -by executing the following command: - -.. code-block:: terminal - - $ composer require symfony/orm-pack - -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: - -.. code-block:: text - - symfony-project/ - ├─ ... - └─ src/ - └─ AppBundle/ - └─ Entity/ - ├─ Comment.php - ├─ Post.php - └─ User.php - -.. tip:: - - If you're more advanced, you can 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\Common\Collections\ArrayCollection; - use Doctrine\ORM\Mapping as ORM; - - /** - * @ORM\Entity - */ - class Post - { - const NUMBER_OF_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="App\Entity\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: - -.. code-block:: terminal - - $ composer require --dev 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 = [ - // ... - ]; - - if (in_array($this->getEnvironment(), ['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: - -.. code-block:: terminal - - $ php bin/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 -:doc:`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. - ----- - -Next: :doc:`/best_practices/controllers` - -.. _`full definition`: https://en.wikipedia.org/wiki/Business_logic -.. _`Doctrine project`: http://www.doctrine-project.org/ -.. _`fixture class`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html#writing-simple-fixtures -.. _`PSR-1`: https://www.php-fig.org/psr/psr-1/ -.. _`PSR-2`: https://www.php-fig.org/psr/psr-2/ -.. _`PHP-CS-Fixer`: https://github.com/FriendsOfPHP/PHP-CS-Fixer diff --git a/best_practices/configuration.rst b/best_practices/configuration.rst deleted file mode 100644 index d6e51c47451..00000000000 --- a/best_practices/configuration.rst +++ /dev/null @@ -1,222 +0,0 @@ -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. - -.. _config-parameters.yml: - -Infrastructure-Related Configuration ------------------------------------- - -.. best-practice:: - - Define the infrastructure-related configuration options in the - ``app/config/parameters.yml`` file. - -The default ``parameters.yml`` file follows this recommendation and defines the -options related to the database and mail server infrastructure: - -.. code-block:: yaml - - # app/config/parameters.yml - parameters: - database_driver: pdo_mysql - database_host: 127.0.0.1 - database_port: ~ - database_name: symfony - database_user: root - database_password: ~ - - mailer_transport: smtp - mailer_host: 127.0.0.1 - mailer_user: ~ - mailer_password: ~ - - # ... - -These options aren't defined inside the ``app/config/config.yml`` file because -they have nothing to do with the application's behavior. In other words, your -application doesn't care about the location of your database or the credentials -to access to it, as long as the database is correctly configured. - -.. _best-practices-canonical-parameters: - -Canonical Parameters -~~~~~~~~~~~~~~~~~~~~ - -.. best-practice:: - - Define all your application's parameters in the - ``app/config/parameters.yml.dist`` file. - -Symfony includes a configuration file called ``parameters.yml.dist``, which -stores the canonical list of configuration parameters for the application. - -Whenever a new configuration parameter is defined for the application, you -should also add it to this file and submit the changes to your version control -system. Then, whenever a developer updates the project or deploys it to a server, -Symfony will check if there is any difference between the canonical -``parameters.yml.dist`` file and your local ``parameters.yml`` file. If there -is a difference, Symfony will ask you to provide a value for the new parameter -and it will add it to your local ``parameters.yml`` file. - -Application-Related Configuration ---------------------------------- - -.. best-practice:: - - Define the application behavior related configuration options in the - ``app/config/config.yml`` file. - -The ``config.yml`` file contains the options used by the application to modify -its behavior, such as the sender of email notifications, or the enabled -`feature toggles`_. Defining these values in ``parameters.yml`` file would -add an extra layer of configuration that's not needed because you don't need -or want these configuration values to change on each server. - -The configuration options defined in the ``config.yml`` file usually vary from -one :doc:`environment ` to another. That's -why Symfony already includes ``app/config/config_dev.yml`` and ``app/config/config_prod.yml`` -files so that you can override specific values for each environment. - -Constants vs Configuration Options -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -One of the most common errors when defining application configuration is to -create new options for values that never change, such as the number of items for -paginated results. - -.. best-practice:: - - Use constants to define configuration options that rarely change. - -The traditional approach for defining configuration options has caused many -Symfony applications to include an option like the following, which would be -used to control the number of posts to display on the blog homepage: - -.. code-block:: yaml - - # app/config/config.yml - parameters: - homepage.number_of_items: 10 - -If you've done something like this in the past, it's likely that you've in fact -*never* actually needed to change that value. Creating a configuration -option for a value that you are never going to configure just isn't necessary. -Our recommendation is to define these values as constants in your application. -You could, for example, define a ``NUMBER_OF_ITEMS`` constant in the ``Post`` entity:: - - // src/AppBundle/Entity/Post.php - namespace AppBundle\Entity; - - class Post - { - const NUMBER_OF_ITEMS = 10; - - // ... - } - -The main advantage of defining constants is that you can use their values -everywhere in your application. When using parameters, they are only available -from places with access to the Symfony container. - -Constants can be used for example in your Twig templates thanks to the -`constant() function`_: - -.. code-block:: html+twig - -

- Displaying the {{ constant('NUMBER_OF_ITEMS', post) }} most recent results. -

- -And Doctrine entities and repositories can access these values too, whereas they -cannot access the container parameters:: - - namespace AppBundle\Repository; - - use AppBundle\Entity\Post; - use Doctrine\ORM\EntityRepository; - - class PostRepository extends EntityRepository - { - public function findLatest($limit = Post::NUMBER_OF_ITEMS) - { - // ... - } - } - -The only notable disadvantage of using constants for this kind of configuration -values is that it's complicated to redefine their values in your tests. - -Parameter Naming ----------------- - -.. best-practice:: - - The name of your configuration parameters should be as short as possible and - should include a common prefix for the entire application. - -Using ``app.`` as the prefix of your parameters is a common practice to avoid -collisions with Symfony and third-party bundles/libraries parameters. Then, use -just one or two words to describe the purpose of the parameter: - -.. code-block:: yaml - - # app/config/config.yml - parameters: - # don't do this: 'dir' is too generic and it doesn't convey any meaning - app.dir: '...' - # do this: short but easy to understand names - app.contents_dir: '...' - # it's OK to use dots, underscores, dashes or nothing, but always - # be consistent and use the same format for all the parameters - app.dir.contents: '...' - app.contents-dir: '...' - -Semantic Configuration: Don't Do It ------------------------------------ - -.. best-practice:: - - Don't define a semantic dependency injection configuration for your bundles. - -As explained in :doc:`/bundles/extension` 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: - -.. code-block:: yaml - - # app/config/config.yml - doctrine: - dbal: - # ... - password: "%env(DB_PASSWORD)%" - -.. versionadded:: 3.2 - - Support for runtime environment variables via the ``%env(...)%`` syntax - was introduced in Symfony 3.2. Prior to version 3.2, you needed to use the - :doc:`special SYMFONY__ variables `. - ----- - -Next: :doc:`/best_practices/business-logic` - -.. _`feature toggles`: https://en.wikipedia.org/wiki/Feature_toggle -.. _`constant() function`: https://twig.symfony.com/doc/2.x/functions/constant.html diff --git a/best_practices/controllers.rst b/best_practices/controllers.rst deleted file mode 100644 index b96facacae3..00000000000 --- a/best_practices/controllers.rst +++ /dev/null @@ -1,226 +0,0 @@ -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 fewer, contain 10 actions or fewer and include 20 lines -of code or fewer 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: - -.. code-block:: yaml - - # 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: - -.. code-block:: text - - / - ├─ ... - └─ 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. We -don't think its benefit is worth the magic, and so recommend against 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 hides the fact -that a controller should always return a Response object (unless you're using a -view layer). - -What does the Controller look like ----------------------------------- - -Considering all this, here is an example of what the controller should look like -for the homepage of our app:: - - namespace AppBundle\Controller; - - use AppBundle\Entity\Post; - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Routing\Annotation\Route; - - class DefaultController extends Controller - { - /** - * @Route("/", name="homepage") - */ - public function indexAction() - { - $posts = $this->getDoctrine() - ->getRepository(Post::class) - ->findLatest(); - - return $this->render('default/index.html.twig', [ - 'posts' => $posts, - ]); - } - } - -Fetching Services ------------------ - -If you extend the base ``Controller`` class, you can access services directly from -the container via ``$this->container->get()`` or ``$this->get()``. But instead, you -should use dependency injection to fetch services by -:ref:`type-hinting action method arguments `: - -.. best-practice:: - - Don't use ``$this->get()`` or ``$this->container->get()`` to fetch services - from the container. Instead, use dependency injection. - -By not fetching services directly from the container, you can make your services -*private*, which has :ref:`several advantages `. - -.. _best-practices-paramconverter: - -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 Symfony\Component\Routing\Annotation\Route; - - /** - * @Route("/{id}", name="admin_post_show") - */ - public function showAction(Post $post) - { - $deleteForm = $this->createDeleteForm($post); - - return $this->render('admin/post/show.html.twig', [ - '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 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The above example 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(Post::class) - ->findOneBy(['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\ParamConverter; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Routing\Annotation\Route; - - /** - * @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 most situations. -However, there is nothing wrong with querying for entities directly if the -ParamConverter would get complicated. - -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 -:doc:`set up before and after filters `. - ----- - -Next: :doc:`/best_practices/templates` - -.. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html diff --git a/best_practices/creating-the-project.rst b/best_practices/creating-the-project.rst deleted file mode 100644 index abcda84b621..00000000000 --- a/best_practices/creating-the-project.rst +++ /dev/null @@ -1,185 +0,0 @@ -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. - -.. best-practice:: - - Use the Symfony Installer to create new Symfony-based projects. - -Read the :doc:`/setup` article learn how to install and use the Symfony -Installer. - -.. _linux-and-mac-os-x-systems: -.. _windows-systems: - -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 and macOS systems**: - -.. class:: command-linux -.. code-block:: terminal - - $ cd projects/ - $ symfony new blog --version=3.4 - -**Windows systems**: - -.. class:: command-windows -.. code-block:: terminal - - > cd projects/ - > php symfony new blog --version=3.4 - -.. note:: - - If the installer doesn't work for you or doesn't output anything, make sure - that the `Phar extension`_ is installed and enabled on your computer. - -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. - -.. tip:: - - 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: - -.. code-block:: text - - blog/ - ├─ app/ - │ ├─ config/ - │ └─ Resources/ - ├─ bin - │ └─ console - ├─ src/ - │ └─ AppBundle/ - ├─ var/ - │ ├─ cache/ - │ ├─ logs/ - │ └─ sessions/ - ├─ tests/ - │ └─ 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/config/``, stores all the configuration defined for any environment; -* ``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; -* ``var/cache/``, stores all the cache files generated by the application; -* ``var/logs/``, stores all the log files generated by the application; -* ``var/sessions/``, stores all the session files generated by the application; -* ``tests/AppBundle/``, stores the automatic tests (e.g. Unit tests) of the - application. -* ``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 -applications 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 -applications, then it shouldn't be its own bundle. Moreover, if 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. - -.. note:: - - 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. - -.. note:: - - Another reason to create a new bundle is when you're overriding something - in a vendor's bundle (e.g. a controller). See :doc:`/bundles/inheritance`. - -All in all, this is the typical directory structure of a Symfony application -that follows these best practices: - -.. code-block:: text - - blog/ - ├─ app/ - │ ├─ config/ - │ └─ Resources/ - ├─ bin/ - │ └─ console - ├─ src/ - │ └─ AppBundle/ - ├─ tests/ - │ └─ AppBundle/ - ├─ var/ - │ ├─ cache/ - │ ├─ logs/ - └─ sessions/ - ├─ vendor/ - └─ web/ - ├─ app.php - └─ app_dev.php - -.. tip:: - - If your Symfony installation doesn't come with a pre-generated AppBundle, - you can generate it by hand executing this command: - - .. code-block:: terminal - - $ php bin/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 -:doc:`override the location of the main directories `: -``cache/``, ``logs/`` and ``web/``. - ----- - -Next: :doc:`/best_practices/configuration` - -.. _`Composer`: https://getcomposer.org/ -.. _`Phar extension`: https://php.net/manual/en/intro.phar.php -.. _`public checksums repository`: https://github.com/sensiolabs/checksums -.. _`these steps`: http://fabien.potencier.org/signing-project-releases.html diff --git a/best_practices/forms.rst b/best_practices/forms.rst deleted file mode 100644 index 0e556216eeb..00000000000 --- a/best_practices/forms.rst +++ /dev/null @@ -1,238 +0,0 @@ -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 AppBundle\Entity\Post; - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\Extension\Core\Type\DateTimeType; - use Symfony\Component\Form\Extension\Core\Type\EmailType; - use Symfony\Component\Form\Extension\Core\Type\TextareaType; - use Symfony\Component\Form\FormBuilderInterface; - use Symfony\Component\OptionsResolver\OptionsResolver; - - class PostType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('title') - ->add('summary', TextareaType::class) - ->add('content', TextareaType::class) - ->add('authorEmail', EmailType::class) - ->add('publishedAt', DateTimeType::class) - ; - } - - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults([ - 'data_class' => Post::class, - ]); - } - } - -.. best-practice:: - - Put the form type classes in the ``AppBundle\Form`` namespace, unless you - use other custom form classes like data transformers. - -To use the class, use ``createForm()`` and pass the fully qualified class name:: - - // ... - use AppBundle\Form\PostType; - - // ... - public function newAction(Request $request) - { - $post = new Post(); - $form = $this->createForm(PostType::class, $post); - - // ... - } - -Registering Forms as Services -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can also :ref:`register your form type as a service `. -This is only needed if your form type requires some dependencies to be injected -by the container, otherwise it is unnecessary overhead and therefore *not* -recommended to do this for all form type classes. - -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. - -The Symfony Form component allows you to 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', SubmitType::class, ['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 AppBundle\Entity\Post; - use AppBundle\Form\PostType; - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Form\Extension\Core\Type\SubmitType; - use Symfony\Component\HttpFoundation\Request; - - class PostController extends Controller - { - // ... - - public function newAction(Request $request) - { - $post = new Post(); - $form = $this->createForm(PostType::class, $post); - $form->add('submit', SubmitType::class, [ - 'label' => 'Create', - 'attr' => ['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: - -.. code-block:: html+twig - - {{ form_start(form) }} - {{ form_widget(form) }} - - - {{ form_end(form) }} - -Validation ----------- - -The :ref:`constraints ` option allows you to -attach :doc:`validation constraints ` to any form field. -However, doing that prevents the validation from being reused in other forms or -other places where the mapped object is used. - -.. best-practice:: - - Do not define your validation constraints in the form but on the object the - form is mapped to. - -For example, to validate that the title of the post edited with a form is not -blank, add the following in the ``Post`` object:: - - // src/Entity/Post.php - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Post - { - /** - * @Assert\NotBlank - */ - public $title; - } - -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 the ``form_widget()`` function to render -all of the fields: - -.. code-block:: twig - - {{ 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 :doc:`/form/form_customization` 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()) { - $entityManager = $this->getDoctrine()->getManager(); - $entityManager->persist($post); - $entityManager->flush(); - - return $this->redirect($this->generateUrl( - 'admin_post_show', - ['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, is it required to call ``$form->isSubmitted()`` in the ``if`` statement -before calling ``isValid()``. Calling ``isValid()`` with an unsubmitted form -is deprecated since version 3.2 and will throw an exception in 4.0. - ----- - -Next: :doc:`/best_practices/i18n` diff --git a/best_practices/i18n.rst b/best_practices/i18n.rst deleted file mode 100644 index 57f3c1c94de..00000000000 --- a/best_practices/i18n.rst +++ /dev/null @@ -1,99 +0,0 @@ -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: - -.. code-block:: yaml - - # app/config/config.yml - framework: - # ... - translator: { fallbacks: ['%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 supports notes in 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. - -.. tip:: - - The `PHP Translation Bundle`_ includes 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: - -.. code-block:: xml - - - - - - - - title.post_list - Post List - - - - - ----- - -Next: :doc:`/best_practices/security` - -.. _`PHP Translation Bundle`: https://github.com/php-translation/symfony-bundle diff --git a/best_practices/index.rst b/best_practices/index.rst deleted file mode 100644 index 8df4abb1364..00000000000 --- a/best_practices/index.rst +++ /dev/null @@ -1,19 +0,0 @@ -Official Symfony Best Practices -=============================== - -.. toctree:: - :hidden: - - introduction - creating-the-project - configuration - business-logic - controllers - templates - forms - i18n - security - web-assets - tests - -.. include:: /best_practices/map.rst.inc diff --git a/best_practices/introduction.rst b/best_practices/introduction.rst deleted file mode 100644 index cb510c34462..00000000000 --- a/best_practices/introduction.rst +++ /dev/null @@ -1,105 +0,0 @@ -.. index:: - single: Symfony Framework 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 applications 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`_. - -.. note:: - - **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 -applications 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 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 :doc:`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, a sample application has been developed with all these -best practices in mind. This project, called the Symfony Demo application, can -be obtained through the Symfony Installer. First, `download and install`_ the -installer and then execute this command to download the demo application: - -.. code-block:: terminal - - $ symfony demo - -**The demo application is a simple blog engine**, because that will allow us to -focus on the Symfony concepts and features without getting buried in difficult -implementation details. Instead of developing the application step by step in -this guide, you'll find selected snippets of code through the chapters. - -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. - ----- - -Next: :doc:`/best_practices/creating-the-project` - -.. _`Fabien Potencier`: https://connect.symfony.com/profile/fabpot -.. _`download and install`: https://symfony.com/download diff --git a/best_practices/map.rst.inc b/best_practices/map.rst.inc deleted file mode 100644 index f9dfd0c3e9d..00000000000 --- a/best_practices/map.rst.inc +++ /dev/null @@ -1,11 +0,0 @@ -* :doc:`/best_practices/introduction` -* :doc:`/best_practices/creating-the-project` -* :doc:`/best_practices/configuration` -* :doc:`/best_practices/business-logic` -* :doc:`/best_practices/controllers` -* :doc:`/best_practices/templates` -* :doc:`/best_practices/forms` -* :doc:`/best_practices/i18n` -* :doc:`/best_practices/security` -* :doc:`/best_practices/web-assets` -* :doc:`/best_practices/tests` diff --git a/best_practices/security.rst b/best_practices/security.rst deleted file mode 100644 index 19ba9f7e330..00000000000 --- a/best_practices/security.rst +++ /dev/null @@ -1,389 +0,0 @@ -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 :doc:`Security guide` 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. If you have separated web and -API sections on your site, you will need more firewall entries. 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. - -.. note:: - - :ref:`Argon2i ` is the hashing algorithm as - recommended by industry standards, but this won't be available to you unless - you are using PHP 7.2+ or have the `libsodium`_ extension installed. - ``bcrypt`` is sufficient for most applications. - -With this in mind, here is the authentication setup from our application, -which uses a login form to load users from the database: - -.. code-block:: yaml - - # 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: login - login_path: login - - logout: - path: security_logout - target: homepage - - # ... access_control exists, but is not shown here - -.. tip:: - - 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 :doc:`security.yml `, the -:ref:`@Security annotation ` and using -:ref:`isGranted ` on the ``security.authorization_checker`` -service directly. - -.. best-practice:: - - * For protecting broad URL patterns, use ``access_control``; - * Whenever possible, use the ``@Security`` annotation; - * Check security directly on the ``security.authorization_checker`` 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. - -.. _best-practices-security-annotation: - -The @Security Annotation ------------------------- - -For controlling access on a controller-by-controller basis, use the ``@Security`` -annotation whenever possible. Placing it above each action makes it consistent and readable. - -In our application, you need the ``ROLE_ADMIN`` in order to create a new post. -Using ``@Security``, this looks like:: - - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; - use Symfony\Component\Routing\Annotation\Route; - // ... - - /** - * Displays a form to create a new Post entity. - * - * @Route("/new", name="admin_post_new") - * @Security("has_role('ROLE_ADMIN')") - */ - public function newAction() - { - // ... - } - -Using Expressions for Complex Security Restrictions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If your security logic is a little bit more complex, you can use an :doc:`expression ` -inside ``@Security``. In the following example, a user can only access the -controller if their email matches the value returned by the ``getAuthorEmail()`` -method on the ``Post`` object:: - - use AppBundle\Entity\Post; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; - use Symfony\Component\Routing\Annotation\Route; - - /** - * @Route("/{id}/edit", name="admin_post_edit") - * @Security("user.getEmail() == post.getAuthorEmail()") - */ - public function editAction(Post $post) - { - // ... - } - -Notice that this requires the use of the `ParamConverter`_, which automatically -queries for the ``Post`` object and puts it on the ``$post`` argument. This -is what makes it possible to use the ``post`` variable in the expression. - -This has one major drawback: an expression in an annotation cannot -be reused in other parts of the application. Imagine that you want to add -a link in a template that will only be seen by authors. Right now you'll -need to repeat the expression code using Twig syntax: - -.. code-block:: html+twig - - {% if app.user and app.user.email == post.authorEmail %} - ... - {% endif %} - -A good solution - if your logic is simple enough - can be to add a new method -to the ``Post`` entity that checks if a given user is its author:: - - // src/AppBundle/Entity/Post.php - // ... - - class Post - { - // ... - - /** - * Is the given User the author of this Post? - * - * @return bool - */ - public function isAuthor(User $user = null) - { - return $user && $user->getEmail() === $this->getAuthorEmail(); - } - } - -Now you can reuse this method both in the template and in the security expression:: - - use AppBundle\Entity\Post; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; - use Symfony\Component\Routing\Annotation\Route; - - /** - * @Route("/{id}/edit", name="admin_post_edit") - * @Security("post.isAuthor(user)") - */ - public function editAction(Post $post) - { - // ... - } - -.. code-block:: html+twig - - {% if post.isAuthor(app.user) %} - ... - {% endif %} - -.. _best-practices-directly-isGranted: -.. _checking-permissions-without-security: -.. _manually-checking-permissions: - -Checking Permissions without @Security --------------------------------------- - -The above example with ``@Security`` only works because we're using the -:ref:`ParamConverter `, which gives the expression -access to the ``post`` variable. If you don't use this, or have some other -more advanced use-case, you can always do the same security check in PHP:: - - /** - * @Route("/{id}/edit", name="admin_post_edit") - */ - public function editAction($id) - { - $post = $this->getDoctrine() - ->getRepository(Post::class) - ->find($id); - - if (!$post) { - throw $this->createNotFoundException(); - } - - if (!$post->isAuthor($this->getUser())) { - $this->denyAccessUnlessGranted('edit', $post); - } - // equivalent code without using the "denyAccessUnlessGranted()" shortcut: - // - // use Symfony\Component\Security\Core\Exception\AccessDeniedException; - // ... - // - // if (!$this->get('security.authorization_checker')->isGranted('edit', $post)) { - // 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 :doc:`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 AppBundle\Entity\Post; - use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; - use Symfony\Component\Security\Core\Authorization\Voter\Voter; - use Symfony\Component\Security\Core\User\UserInterface; - - class PostVoter extends Voter - { - const CREATE = 'create'; - const EDIT = 'edit'; - - /** - * @var AccessDecisionManagerInterface - */ - private $decisionManager; - - public function __construct(AccessDecisionManagerInterface $decisionManager) - { - $this->decisionManager = $decisionManager; - } - - protected function supports($attribute, $subject) - { - if (!in_array($attribute, [self::CREATE, self::EDIT])) { - return false; - } - - if (!$subject instanceof Post) { - return false; - } - - return true; - } - - protected function voteOnAttribute($attribute, $subject, TokenInterface $token) - { - $user = $token->getUser(); - /** @var Post */ - $post = $subject; // $subject must be a Post instance, thanks to the supports method - - if (!$user instanceof UserInterface) { - return false; - } - - switch ($attribute) { - case self::CREATE: - // if the user is an admin, allow them to create new posts - if ($this->decisionManager->decide($token, ['ROLE_ADMIN'])) { - return true; - } - - break; - case self::EDIT: - // if the user is the author of the post, allow them to edit the posts - if ($user->getEmail() === $post->getAuthorEmail()) { - return true; - } - - break; - } - - return false; - } - } - -If you're using the :ref:`default services.yml configuration `, -your application will :ref:`autoconfigure ` your security -voter and inject an ``AccessDecisionManagerInterface`` instance into it thanks to -:doc:`autowiring `. - -Now, you can use the voter with the ``@Security`` annotation:: - - /** - * @Route("/{id}/edit", name="admin_post_edit") - * @Security("is_granted('edit', post)") - */ - public function editAction(Post $post) - { - // ... - } - -You can also use this directly with the ``security.authorization_checker`` service or -via the even easier shortcut in a controller:: - - /** - * @Route("/{id}/edit", name="admin_post_edit") - */ - public function editAction($id) - { - $post = ...; // query for the post - - $this->denyAccessUnlessGranted('edit', $post); - - // or without the shortcut: - // - // use Symfony\Component\Security\Core\Exception\AccessDeniedException; - // ... - // - // if (!$this->get('security.authorization_checker')->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 :doc:`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 :doc:`impersonate users `. - -If your company uses a user login method not supported by Symfony, you can -develop :doc:`your own user provider ` and -:doc:`your own authentication provider `. - ----- - -Next: :doc:`/best_practices/web-assets` - -.. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html -.. _`FOSUserBundle`: https://github.com/FriendsOfSymfony/FOSUserBundle -.. _`libsodium`: https://pecl.php.net/package/libsodium diff --git a/best_practices/templates.rst b/best_practices/templates.rst deleted file mode 100644 index b1327a72fdf..00000000000 --- a/best_practices/templates.rst +++ /dev/null @@ -1,156 +0,0 @@ -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 Twig namespaces -to refer to them (e.g. ``@AcmeDemo/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/`` -============================================ ================================== -``@AcmeDemo/index.html.twig`` ``index.html.twig`` -``@AcmeDemo/Default/index.html.twig`` ``default/index.html.twig`` -``@AcmeDemo/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. - -.. best-practice:: - - Use lowercase snake_case for directory and template names. - -.. best-practice:: - - Use a prefixed underscore for partial templates in template names. - -You often want to reuse template code using the ``include`` function to avoid -redundant code. To determine those partials in the filesystem you should -prefix partials and any other template without HTML body or ``extends`` tag -with a single underscore. - -Twig Extensions ---------------- - -.. best-practice:: - - Define your Twig extensions in the ``AppBundle/Twig/`` directory. Your - application will automatically detect them and configure them. - -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: - -.. code-block:: terminal - - $ composer require erusev/parsedown - -Then, create a new ``Markdown`` class that will be used later by the Twig -extension. It 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) - { - return $this->parser->text($text); - } - } - -Next, create a new Twig extension and define a new filter called ``md2html`` -using the ``Twig\TwigFilter`` class. Inject the newly defined ``Markdown`` -class in the constructor of the Twig extension:: - - namespace AppBundle\Twig; - - use AppBundle\Utils\Markdown; - use Twig\Extension\AbstractExtension; - use Twig\TwigFilter; - - class AppExtension extends AbstractExtension - { - private $parser; - - public function __construct(Markdown $parser) - { - $this->parser = $parser; - } - - public function getFilters() - { - return [ - new TwigFilter( - 'md2html', - [$this, 'markdownToHtml'], - ['is_safe' => ['html'], 'pre_escape' => 'html'] - ), - ]; - } - - public function markdownToHtml($content) - { - return $this->parser->toHtml($content); - } - - public function getName() - { - return 'app_extension'; - } - } - -And that's it! - -If you're using the :ref:`default services.yml configuration `, -you're done! Symfony will automatically know about your new service and tag it to -be used as a Twig extension. - ----- - -Next: :doc:`/best_practices/forms` - -.. _`Twig`: https://twig.symfony.com/ -.. _`Parsedown`: https://parsedown.org/ diff --git a/best_practices/tests.rst b/best_practices/tests.rst deleted file mode 100644 index f6f67685535..00000000000 --- a/best_practices/tests.rst +++ /dev/null @@ -1,122 +0,0 @@ -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 like this is simple to implement thanks to -:ref:`PHPUnit data providers `:: - - // tests/AppBundle/ApplicationAvailabilityFunctionalTest.php - namespace Tests\AppBundle; - - use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - - class ApplicationAvailabilityFunctionalTest extends WebTestCase - { - /** - * @dataProvider urlProvider - */ - public function testPageIsSuccessful($url) - { - $client = self::createClient(); - $client->request('GET', $url); - - $this->assertTrue($client->getResponse()->isSuccessful()); - } - - public function urlProvider() - { - return [ - ['/'], - ['/posts'], - ['/post/fixture-post-1'], - ['/blog/category/fixture-category'], - ['/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. - -If you have a heavy JavaScript frontend, you should consider using pure -JavaScript-based testing tools. - -Learn More about Functional Tests ---------------------------------- - -Consider using the `HautelookAliceBundle`_ to generate real-looking data for -your test fixtures using `Faker`_ and `Alice`_. - -.. _`PHPUnit`: https://phpunit.de/ -.. _`PHPSpec`: https://www.phpspec.net/ -.. _`smoke testing`: https://en.wikipedia.org/wiki/Smoke_testing_(software) -.. _`Mink`: http://mink.behat.org -.. _`HautelookAliceBundle`: https://github.com/hautelook/AliceBundle -.. _`Faker`: https://github.com/fzaninotto/Faker -.. _`Alice`: https://github.com/nelmio/alice diff --git a/best_practices/web-assets.rst b/best_practices/web-assets.rst deleted file mode 100644 index c567bf36e47..00000000000 --- a/best_practices/web-assets.rst +++ /dev/null @@ -1,102 +0,0 @@ -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: - -.. code-block:: html+twig - - - - - {# ... #} - - - - -.. note:: - - Keep in mind that ``web/`` is a public directory and that anything stored - here will be publicly accessible, including all the original asset files - (e.g. Sass, LESS and CoffeeScript files). - -Using Assetic -------------- - -.. include:: /assetic/_standard_edition_warning.rst.inc - -These days, you probably can't create static CSS and JavaScript files and -include them in your template without much effort. 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. - -:doc:`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: - -.. code-block:: html+twig - - {% stylesheets - 'css/bootstrap.min.css' - 'css/main.css' - filter='cssrewrite' output='css/compiled/app.css' %} - - {% endstylesheets %} - - {# ... #} - - {% javascripts - 'js/jquery.min.js' - 'js/bootstrap.min.js' - output='js/compiled/app.js' %} - - {% 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 -:doc:`using UglifyCSS/UglifyJS ` to speed up your -websites. You can even :doc:`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. - ----- - -Next: :doc:`/best_practices/tests` - -.. _`official Assetic documentation`: https://github.com/kriswallsmith/assetic diff --git a/bundles.rst b/bundles.rst index 43ccbb10111..21ecea9f606 100644 --- a/bundles.rst +++ b/bundles.rst @@ -8,73 +8,43 @@ The Bundle System .. caution:: - In Symfony versions prior to 3.4, it was recommended to organize your own + In Symfony versions prior to 4.0, it was recommended to organize your own application code using bundles. This is no longer recommended and bundles should only be used to share code and features between multiple applications. -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. - -.. note:: - - While you'll learn the basics here, an entire article is devoted to the - organization and best practices of :doc:`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, JavaScript files, tests and anything else. -Every aspect of a feature exists in a bundle and every feature lives in a -bundle. - -Bundles used in your applications must be enabled by registering them in -the ``registerBundles()`` method of the ``AppKernel`` class:: - - // app/AppKernel.php - public function registerBundles() - { - $bundles = [ - 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 Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), - new AppBundle\AppBundle(), - ]; - - if (in_array($this->getEnvironment(), ['dev', 'test'])) { - $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 is similar to a plugin in other software, but even better. The core +features of Symfony framework are implemented with bundles (FrameworkBundle, +SecurityBundle, DebugBundle, etc.) They are also used to add new features in +your application via `third-party bundles`_. + +Bundles used in your applications must be enabled per +:ref:`environment ` in the ``config/bundles.php`` +file:: + + // config/bundles.php + return [ + // 'all' means that the bundle is enabled for any Symfony environment + Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], + Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], + // this bundle is enabled only in 'dev' and 'test', so you can't use it in 'prod' + Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + ]; .. tip:: - A bundle can live *anywhere* as long as it can be autoloaded (via the - autoloader configured at ``app/autoload.php``). + In a default Symfony application that uses :ref:`Symfony Flex `, + bundles are enabled/disabled automatically for you when installing/removing + them, so you don't need to look at or edit this ``bundles.php`` file. Creating a Bundle ----------------- -`SensioGeneratorBundle`_ is an optional bundle that includes commands to create -different elements of your application, such as bundles. If you create lots of -bundles, consider using it. However, this section creates and enables a new -bundle by hand to show how simple it is to do it. - +This section creates and enables a new bundle to show there are only a few steps required. The new bundle is called AcmeTestBundle, where 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``). @@ -83,7 +53,7 @@ Start by creating a ``src/Acme/TestBundle/`` directory and adding a new file called ``AcmeTestBundle.php``:: // src/Acme/TestBundle/AcmeTestBundle.php - namespace Acme\TestBundle; + namespace App\Acme\TestBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -100,50 +70,22 @@ called ``AcmeTestBundle.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 = [ - // ... +of the bundle. Now that you've created the bundle, enable it:: - // register your bundle - new Acme\TestBundle\AcmeTestBundle(), - ]; + // config/bundles.php + return [ // ... - - return $bundles; - } + App\Acme\TestBundle\AcmeTestBundle::class => ['all' => true], + ]; 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: - -.. code-block:: terminal - - $ php bin/console generate:bundle --namespace=Acme/TestBundle - -The bundle skeleton generates a basic controller, template and routing -resource that can be customized. You'll learn more about Symfony's command-line -tools later. - -.. tip:: - - 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: +The directory structure of a bundle is meant to help to keep code consistent +between all Symfony bundles. It follows a set of conventions, but is flexible +to be adjusted if needed: ``Controller/`` Contains the controllers of the bundle (e.g. ``RandomController.php``). @@ -154,14 +96,14 @@ of the most common elements of a bundle: necessary). ``Resources/config/`` - Houses configuration, including routing configuration (e.g. ``routing.yml``). + Houses configuration, including routing configuration (e.g. ``routing.yaml``). ``Resources/views/`` Holds templates organized by controller name (e.g. ``Random/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 + linked into the project ``public/`` directory via the ``assets:install`` console command. ``Tests/`` @@ -178,11 +120,10 @@ the bundle. Learn more ---------- -.. toctree:: - :maxdepth: 1 - :glob: - - bundles/* +* :doc:`/bundles/override` +* :doc:`/bundles/best_practices` +* :doc:`/bundles/configuration` +* :doc:`/bundles/extension` +* :doc:`/bundles/prepend_extension` .. _`third-party bundles`: https://github.com/search?q=topic%3Asymfony-bundle&type=Repositories -.. _`SensioGeneratorBundle`: https://symfony.com/doc/current/bundles/SensioGeneratorBundle/index.html diff --git a/bundles/best_practices.rst b/bundles/best_practices.rst index c9b68c1443b..3dcd90036dc 100644 --- a/bundles/best_practices.rst +++ b/bundles/best_practices.rst @@ -4,21 +4,10 @@ Best Practices for Reusable Bundles =================================== -There are two 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** to be -configurable and extendable. 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 guides. - -.. seealso:: - - The best practices for application-specific bundles are discussed in - :doc:`/best_practices/introduction`. +configurable and extendable. Reusable bundles are those meant to be shared +privately across many company projects or publicly so any Symfony project can +install them. .. index:: pair: Bundle; Naming conventions @@ -28,13 +17,13 @@ the guides. Bundle Name ----------- -A bundle is also a PHP namespace. The namespace must follow the `PSR-0`_ or -`PSR-4`_ interoperability standards for PHP 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 ``Bundle``. +A bundle is also a PHP namespace. The namespace must follow the `PSR-4`_ +interoperability standard for PHP 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 ``Bundle``. A namespace becomes a bundle as soon as you add a bundle class to it. The -bundle class name must follow these simple rules: +bundle class name must follow these rules: * Use only alphanumeric characters and underscores; * Use a StudlyCaps name (i.e. camelCase with an uppercase first letter); @@ -124,8 +113,8 @@ Service Container Extensions ``DependencyInjection/`` Doctrine ORM entities (when not using annotations) ``Entity/`` Doctrine ODM documents (when not using annotations) ``Document/`` Event Listeners ``EventListener/`` -Configuration ``Resources/config/`` -Web Resources (CSS, JS, images) ``Resources/public/`` +Configuration (routes, services, etc.) ``Resources/config/`` +Web Assets (CSS, JS, images) ``Resources/public/`` Translation files ``Resources/translations/`` Validation (when not using annotations) ``Resources/config/validation/`` Serialization (when not using annotations) ``Resources/config/serialization/`` @@ -177,6 +166,89 @@ the ``Tests/`` directory. Tests should follow the following principles: A test suite must not contain ``AllTests.php`` scripts, but must rely on the existence of a ``phpunit.xml.dist`` file. +Continuous Integration +---------------------- + +Testing bundle code continuously, including all its commits and pull requests, +is a good practice called Continuous Integration. There are several services +providing this feature for free for open source projects. The most popular +service for Symfony bundles is called `Travis CI`_. + +Here is the recommended configuration file (``.travis.yml``) for Symfony bundles, +which test the two latest :doc:`LTS versions ` +of Symfony and the latest beta release: + +.. code-block:: yaml + + language: php + sudo: false + cache: + directories: + - $HOME/.composer/cache/files + - $HOME/symfony-bridge/.phpunit + + env: + global: + - PHPUNIT_FLAGS="-v" + - SYMFONY_PHPUNIT_DIR="$HOME/symfony-bridge/.phpunit" + + matrix: + fast_finish: true + include: + # Minimum supported dependencies with the latest and oldest PHP version + - php: 7.2 + env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" SYMFONY_DEPRECATIONS_HELPER="max[self]=0" + - php: 7.1 + env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" SYMFONY_DEPRECATIONS_HELPER="max[self]=0" + + # Test the latest stable release + - php: 7.1 + - php: 7.2 + env: COVERAGE=true PHPUNIT_FLAGS="-v --coverage-text" + + # Test LTS versions. This makes sure we do not use Symfony packages with version greater + # than 2 or 3 respectively. Read more at https://github.com/symfony/lts + - php: 7.2 + env: DEPENDENCIES="symfony/lts:^2" + - php: 7.2 + env: DEPENDENCIES="symfony/lts:^3" + + # Latest commit to master + - php: 7.2 + env: STABILITY="dev" + + allow_failures: + # Dev-master is allowed to fail. + - env: STABILITY="dev" + + before_install: + - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi + - if ! [ -z "$STABILITY" ]; then composer config minimum-stability ${STABILITY}; fi; + - if ! [ -v "$DEPENDENCIES" ]; then composer require --no-update ${DEPENDENCIES}; fi; + + install: + - composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction + - ./vendor/bin/simple-phpunit install + + script: + - composer validate --strict --no-check-lock + # simple-phpunit is the PHPUnit wrapper provided by the PHPUnit Bridge component and + # it helps with testing legacy code and deprecations (composer require symfony/phpunit-bridge) + - ./vendor/bin/simple-phpunit $PHPUNIT_FLAGS + +Consider using the `Travis cron`_ tool to make sure your project is built even if +there are no new pull requests or commits. + +Installation +------------ + +Bundles should set ``"type": "symfony-bundle"`` in their ``composer.json`` file. +With this, :ref:`Symfony Flex ` will be able to automatically +enable your bundle when it's installed. + +If your bundle requires any setup (e.g. configuration, new files, changes to +``.gitignore``, etc), then you should create a `Symfony Flex recipe`_. + Documentation ------------- @@ -207,8 +279,19 @@ following standardized instructions in your ``README.md`` file. [installation chapter](https://getcomposer.org/doc/00-intro.md) of the Composer documentation. - Step 1: Download the Bundle - --------------------------- + Applications that use Symfony Flex + ---------------------------------- + + Open a command console, enter your project directory and execute: + + ```console + $ composer require + ``` + + Applications that don't use Symfony Flex + ---------------------------------------- + + ### 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: @@ -217,30 +300,18 @@ following standardized instructions in your ``README.md`` file. $ composer require ``` - Step 2: Enable the Bundle - ------------------------- + ### Step 2: Enable the Bundle Then, enable the bundle by adding it to the list of registered bundles - in the `app/AppKernel.php` file of your project: + in the `config/bundles.php` file of your project: ```php - // app/AppKernel.php - - // ... - class AppKernel extends Kernel - { - public function registerBundles() - { - $bundles = [ - // ... - new \\(), - ]; - - // ... - } + // config/bundles.php + return [ // ... - } + \\::class => ['all' => true], + ]; ``` .. code-block:: rst @@ -251,8 +322,19 @@ following standardized instructions in your ``README.md`` file. Make sure Composer is installed globally, as explained in the `installation chapter`_ of the Composer documentation. + ---------------------------------- + + Open a command console, enter your project directory and execute: + + .. code-block:: bash + + $ composer require + + Applications that don't use Symfony Flex + ---------------------------------------- + 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: @@ -262,29 +344,16 @@ following standardized instructions in your ``README.md`` file. $ composer require Step 2: Enable the Bundle - ------------------------- + ~~~~~~~~~~~~~~~~~~~~~~~~~ Then, enable the bundle by adding it to the list of registered bundles - in the ``app/AppKernel.php`` file of your project:: - - // app/AppKernel.php - - // ... - class AppKernel extends Kernel - { - public function registerBundles() - { - $bundles = [ - // ... - - new \\(), - ]; - - // ... - } + in the ``config/bundles.php`` file of your project:: + // config/bundles.php + return [ // ... - } + \\::class => ['all' => true], + ]; .. _`installation chapter`: https://getcomposer.org/doc/00-intro.md @@ -338,13 +407,13 @@ The end user can provide values in any configuration file: .. code-block:: yaml - # app/config/config.yml + # config/services.yaml parameters: acme_blog.author.email: 'fabien@example.com' .. code-block:: xml - + setParameter('acme_blog.author.email', 'fabien@example.com'); Retrieve the configuration parameters in your code from the container:: $container->getParameter('acme_blog.author.email'); -Even if this mechanism is simple enough, you should consider using the more -advanced :doc:`semantic bundle configuration `. +While this mechanism requires the least effort, you should consider using the +more advanced :doc:`semantic bundle configuration ` to +make your configuration more robust. Versioning ---------- @@ -377,8 +447,11 @@ Bundles must be versioned following the `Semantic Versioning Standard`_. Services -------- -If the bundle defines services, they must be prefixed with the bundle alias. -For example, AcmeBlogBundle services must be prefixed with ``acme_blog``. +If the bundle defines services, they must be prefixed with the bundle alias +instead of using fully qualified class names like you do in your project +services. For example, AcmeBlogBundle services must be prefixed with ``acme_blog``. +The reason is that bundles shouldn't rely on features such as service autowiring +or autoconfiguration to not impose an overhead when compiling application services. In addition, services not meant to be used by the application directly, should be :ref:`defined as private `. For public services, @@ -418,9 +491,13 @@ The ``composer.json`` file should include at least the following metadata: a string (or array of strings) with a `valid license identifier`_, such as ``MIT``. ``autoload`` - This information is used by Symfony to load the classes of the bundle. The - `PSR-4`_ autoload standard is recommended for modern bundles, but `PSR-0`_ - standard is also supported. + This information is used by Symfony to load the classes of the bundle. It's + recommended to use the `PSR-4`_ autoload standard: use the namespace as key, + and the location of the bundle's main class (relative to ``composer.json``) + as value. For example, if the main class is located in the bundle root + directory: ``"autoload": { "psr-4": { "SomeVendor\\BlogBundle\\": "" } }``. + If the main class is located in the ``src/`` directory of the bundle: + ``"autoload": { "psr-4": { "SomeVendor\\BlogBundle\\": "src/" } }``. In order to make it easier for developers to find your bundle, register it on `Packagist`_, the official repository for Composer packages. @@ -430,7 +507,7 @@ Resources If the bundle references any resources (config files, translation files, etc.), don't use physical paths (e.g. ``__DIR__/config/services.xml``) but logical -paths (e.g. ``@AppBundle/Resources/config/services.xml``). +paths (e.g. ``@FooBundle/Resources/config/services.xml``). The logical paths are required because of the bundle overriding mechanism that lets you override any resource/file of any bundle. See :ref:`http-kernel-resource-locator` @@ -438,7 +515,7 @@ for more details about transforming physical paths into logical paths. Beware that templates use a simplified version of the logical path shown above. For example, an ``index.html.twig`` template located in the ``Resources/views/Default/`` -directory of the AppBundle, is referenced as ``@App/Default/index.html.twig``. +directory of the FooBundle, is referenced as ``@Foo/Default/index.html.twig``. Learn more ---------- @@ -446,9 +523,11 @@ Learn more * :doc:`/bundles/extension` * :doc:`/bundles/configuration` -.. _`PSR-0`: https://www.php-fig.org/psr/psr-0/ .. _`PSR-4`: https://www.php-fig.org/psr/psr-4/ +.. _`Symfony Flex recipe`: https://github.com/symfony/recipes .. _`Semantic Versioning Standard`: https://semver.org/ .. _`Packagist`: https://packagist.org/ .. _`choose any license`: https://choosealicense.com/ .. _`valid license identifier`: https://spdx.org/licenses/ +.. _`Travis CI`: https://travis-ci.org/ +.. _`Travis cron`: https://docs.travis-ci.com/user/cron-jobs/ diff --git a/bundles/configuration.rst b/bundles/configuration.rst index 2521aa53274..9da8c884370 100644 --- a/bundles/configuration.rst +++ b/bundles/configuration.rst @@ -5,11 +5,12 @@ 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 sections, such as ``framework``, -``twig`` and ``doctrine``. Each of these configures a specific bundle, allowing -you to define options at a high level and then let the bundle make all the -low-level, complex changes based on your settings. +If you open your main application configuration directory (usually +``config/packages/``), you'll see a number of different files, such as +``framework.yaml``, ``twig.yaml`` and ``doctrine.yaml``. Each of these +configures a specific bundle, allowing you to define options at a high level and +then let the bundle make all the low-level, complex changes based on your +settings. For example, the following configuration tells the FrameworkBundle to enable the form integration, which involves the definition of quite a few services as well @@ -43,29 +44,18 @@ as integration of other related components: 'form' => true, ]); -.. sidebar:: Using Parameters to Configure your Bundle - - If you don't have plans to share your bundle between projects, it doesn't - make sense to use this more advanced way of configuration. Since you use - the bundle only in one project, you can just change the service - configuration each time. - - If you *do* want to be able to configure something from within - ``config.yml``, you can always create a parameter there and use that - parameter somewhere else. - Using the Bundle Extension -------------------------- Imagine you are creating a new bundle - AcmeSocialBundle - which provides -integration with Twitter, etc. To make your bundle easy to use, you want to -allow users to configure it with some configuration that looks like this: +integration with Twitter. To make your bundle configurable to the user, you +can add some configuration that looks like this: .. configuration-block:: .. code-block:: yaml - # app/config/config.yml + # config/packages/acme_social.yaml acme_social: twitter: client_id: 123 @@ -73,7 +63,7 @@ allow users to configure it with some configuration that looks like this: .. code-block:: xml - + loadFromExtension('acme_social', [ 'twitter' => [ 'client_id' => 123, @@ -151,20 +141,20 @@ For the configuration example in the previous section, the array passed to your ] 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:: +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/packages/dev/acme_social.yaml`` - with +different values beneath it, the incoming array might look like this:: [ - // values from config.yml + // values from config/packages/acme_social.yaml [ 'twitter' => [ 'client_id' => 123, 'client_secret' => 'your_secret', ], ], - // values from config_dev.yml + // values from config/packages/dev/acme_social.yaml [ 'twitter' => [ 'client_id' => 456, @@ -192,10 +182,9 @@ The ``Configuration`` class to handle the sample configuration looks like:: { public function getConfigTreeBuilder() { - $treeBuilder = new TreeBuilder(); - $rootNode = $treeBuilder->root('acme_social'); + $treeBuilder = new TreeBuilder('acme_social'); - $rootNode + $treeBuilder->getRootNode() ->children() ->arrayNode('twitter') ->children() @@ -308,7 +297,7 @@ In your extension, you can load this and dynamically set its arguments:: .. sidebar:: Processing the Configuration yourself Using the Config component is fully optional. The ``load()`` method gets an - array of configuration values. You can simply parse these arrays yourself + array of configuration values. You can instead parse these arrays yourself (e.g. by overriding configurations and using :phpfunction:`isset` to check for the existence of a value). Be aware that it'll be very hard to support XML:: @@ -326,12 +315,10 @@ In your extension, you can load this and dynamically set its arguments:: 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 -:doc:`/bundles/prepend_extension`. +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. This can be achieved using a prepend extension. +For more details, see :doc:`/bundles/prepend_extension`. Dump the Configuration ---------------------- @@ -403,7 +390,7 @@ namespace is then replaced with the XSD validation base path returned from 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 +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 @@ -424,7 +411,7 @@ Assuming the XSD file is called ``hello-1.0.xsd``, the schema location will be .. code-block:: xml - + ` -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 +method 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 ``config/packages/`` files). By default, this is done by removing the ``Extension`` suffix and converting the class name to underscores (e.g. ``AcmeHelloExtension``'s DI alias is ``acme_hello``). @@ -87,11 +84,10 @@ 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! +(using the YAML, XML or PHP format). For instance, assume you have a file called ``services.xml`` in the -``Resources/config`` directory of your bundle, your ``load()`` method looks like:: +``Resources/config/`` directory of your bundle, your ``load()`` method looks like:: use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -106,67 +102,33 @@ For instance, assume you have a file called ``services.xml`` in the $loader->load('services.xml'); } -Other available loaders are the ``YamlFileLoader``, ``PhpFileLoader`` and -``IniFileLoader``. - -.. note:: - - The ``IniFileLoader`` can only be used to load parameters and it can only - load them as strings. - -.. caution:: - - If you removed the default file with service definitions (i.e. - ``app/config/services.yml``), make sure to also remove it from the - ``imports`` key in ``app/config/config.yml``. +The other available loaders are ``YamlFileLoader`` and ``PhpFileLoader``. Using Configuration to Change the Services ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The Extension is also the class that handles the configuration for that -particular bundle (e.g. the configuration in ``app/config/config.yml``). To -read more about it, see the ":doc:`/bundles/configuration`" article. +particular bundle (e.g. the configuration in ``config/packages/.yaml``). +To read more about it, see the ":doc:`/bundles/configuration`" article. Adding Classes to Compile ------------------------- -.. deprecated:: 3.3 - - This technique is discouraged and the ``addClassesToCompile()`` method was - deprecated in Symfony 3.3 because modern PHP versions make it unnecessary. - -Symfony creates a big ``classes.php`` file in the cache directory to aggregate -the contents of the PHP classes that are used in every request. This reduces the -I/O operations and increases the application performance. +Bundles can hint Symfony about which of their classes contain annotations so +they are compiled when generating the application cache to improve the overall +performance. Define the list of annotated classes to compile in the +``addAnnotatedClassesToCompile()`` method:: -.. versionadded:: 3.2 - - The ``addAnnotatedClassesToCompile()`` method was introduced in Symfony 3.2. - -Your bundles can also add their own classes into this file thanks to the -``addClassesToCompile()`` and ``addAnnotatedClassesToCompile()`` methods (both -work in the same way, but the second one is for classes that contain PHP -annotations). Define the classes to compile as an array of their fully qualified -class names:: - - use AppBundle\Manager\UserManager; - use AppBundle\Utils\Slugger; - - // ... public function load(array $configs, ContainerBuilder $container) { // ... - // this method can't compile classes that contain PHP annotations - $this->addClassesToCompile([ - UserManager::class, - Slugger::class, - // ... - ]); - - // add here only classes that contain PHP annotations $this->addAnnotatedClassesToCompile([ - 'AppBundle\\Controller\\DefaultController', + // you can define the fully qualified class names... + 'App\\Controller\\DefaultController', + // ... but glob patterns are also supported: + '**Bundle\\Controller\\', + // ... ]); } @@ -176,28 +138,6 @@ class names:: If some class extends from other classes, all its parents are automatically included in the list of classes to compile. -.. versionadded:: 3.2 - - The option to add classes to compile using patterns was introduced in Symfony 3.2. - -The classes to compile can also be added using file path patterns:: - - // ... - public function load(array $configs, ContainerBuilder $container) - { - // ... - - $this->addClassesToCompile([ - '**Bundle\\Manager\\', - // ... - ]); - - $this->addAnnotatedClassesToCompile([ - '**Bundle\\Controller\\', - // ... - ]); - } - Patterns are transformed into the actual class namespaces using the classmap generated by Composer. Therefore, before using these patterns, you must generate the full classmap executing the ``dump-autoload`` command of Composer. diff --git a/bundles/index.rst b/bundles/index.rst index 87641de5e23..e4af2cd357b 100644 --- a/bundles/index.rst +++ b/bundles/index.rst @@ -1,14 +1,13 @@ +:orphan: + Bundles ======= .. toctree:: :maxdepth: 2 - installation - best_practices - inheritance override - remove - extension + best_practices configuration + extension prepend_extension diff --git a/bundles/inheritance.rst b/bundles/inheritance.rst deleted file mode 100644 index 5720a944b88..00000000000 --- a/bundles/inheritance.rst +++ /dev/null @@ -1,113 +0,0 @@ -.. index:: - single: Bundle; Inheritance - -How to Use Bundle Inheritance to Override Parts of a Bundle -=========================================================== - -.. deprecated:: 3.4 - - Bundle inheritance is deprecated since Symfony 3.4 and will be removed in - 4.0, but you can :doc:`override any part of a bundle ` - without using bundle inheritance. - -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 have installed `FOSUserBundle`_, but you want to -override its base ``layout.html.twig`` template, as well as one of its -controllers. - -First, create a new bundle called UserBundle and enable it in your application. -Then, register the third-party FOSUserBundle as the "parent" of your bundle:: - - // src/UserBundle/UserBundle.php - namespace UserBundle; - - use Symfony\Component\HttpKernel\Bundle\Bundle; - - class UserBundle extends Bundle - { - public function getParent() - { - return 'FOSUserBundle'; - } - } - -By making this small change, you can now override several parts of the FOSUserBundle -by creating a file with the same name. - -.. note:: - - 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/UserBundle/Controller/RegistrationController.php - namespace UserBundle\Controller; - - use FOS\UserBundle\Controller\RegistrationController as BaseController; - - class RegistrationController extends BaseController - { - public function registerAction() - { - $response = parent::registerAction(); - - // ... do custom stuff - return $response; - } - } - -.. tip:: - - Depending on how severely you need to change the behavior, you might - call ``parent::registerAction()`` or completely replace its logic with - your own. - -.. note:: - - 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 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 UserBundle. 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. - -.. note:: - - The overriding of resources only works when you refer to resources with - the ``@FOSUserBundle/Resources/config/routing/security.xml`` method. - You need to use the ``@BundleName`` shortcut when referring to resources - so they can be successfully overridden (except templates, which are - overridden in a different way, as explained in :doc:`/templating/overriding`). - -.. caution:: - - Translation and validation files do not work in the same way as described - above. Read ":ref:`override-translations`" if you want to learn how to - override translations and see ":ref:`override-validation`" for tricks to - override the validation. - -.. _`FOSUserBundle`: https://github.com/friendsofsymfony/fosuserbundle diff --git a/bundles/installation.rst b/bundles/installation.rst deleted file mode 100644 index c8f3e4ec1d5..00000000000 --- a/bundles/installation.rst +++ /dev/null @@ -1,160 +0,0 @@ -.. index:: - single: Bundle; Installation - -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`_ -* `B) Enable the Bundle`_ -* `C) Configure the Bundle`_ - -A) Add Composer Dependencies ----------------------------- - -Dependencies are managed with Composer, so if Composer is new to you, learn -some basics in `their documentation`_. This involves two steps: - -1) Find out the Name of the Bundle on Packagist -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The README for a bundle (e.g. `FOSUserBundle`_) usually tells you its name -(e.g. ``friendsofsymfony/user-bundle``). If it doesn't, you can search for -the bundle on the `Packagist.org`_ site. - -.. tip:: - - Looking for bundles? Try searching for `symfony-bundle topic on GitHub`_. - -2) Install the Bundle via Composer -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Now that you know the package name, you can install it via Composer: - -.. code-block:: terminal - - $ composer require friendsofsymfony/user-bundle - -This will choose the best version for your project, add it to ``composer.json`` -and download its code into the ``vendor/`` directory. If you need a specific -version, include it as the second argument of the `composer require`_ command: - -.. code-block:: terminal - - $ composer require friendsofsymfony/user-bundle "~2.0" - -B) Enable the Bundle --------------------- - -At this point, the bundle is installed in your Symfony project (e.g. -``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 = [ - // ... - new FOS\UserBundle\FOSUserBundle(), - ]; - - // ... - } - } - -In a few rare cases, you may want a bundle to be *only* enabled in the development -:doc:`environment `. For example, -the DoctrineFixturesBundle helps to load dummy data - something you probably -only want to do while developing. To only load this bundle in the ``dev`` -and ``test`` environments, register the bundle in this way:: - - // app/AppKernel.php - - // ... - class AppKernel extends Kernel - { - // ... - - public function registerBundles() - { - $bundles = [ - // ... - ]; - - if (in_array($this->getEnvironment(), ['dev', 'test'])) { - $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(); - } - - // ... - } - } - -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 configuration -via the ``config:dump-reference`` command: - -.. code-block:: terminal - - $ php bin/console config:dump-reference AsseticBundle - -Instead of the full bundle name, you can also pass the short name used as the root -of the bundle's configuration: - -.. code-block:: terminal - - $ php bin/console config:dump-reference assetic - -The output will look like this: - -.. code-block:: yaml - - assetic: - debug: '%kernel.debug%' - use_controller: - enabled: '%kernel.debug%' - profiler: false - read_from: '%kernel.project_dir%/web' - write_to: '%assetic.read_from%' - java: /usr/bin/java - node: /usr/local/bin/node - node_paths: [] - # ... - -.. tip:: - - For complex bundles that define lots of configuration options, you can pass - a second optional argument to the ``config:dump-reference`` command to only - display a section of the entire configuration: - - .. code-block:: terminal - - $ php bin/console config:dump-reference AsseticBundle use_controller - - # Default configuration for "AsseticBundle" at path "use_controller" - use_controller: - enabled: '%kernel.debug%' - profiler: false - -Other Setup ------------ - -At this point, check the ``README`` file of your brand new bundle to see -what to do next. Have fun! - -.. _their documentation: https://getcomposer.org/doc/00-intro.md -.. _Packagist.org: https://packagist.org -.. _FOSUserBundle: https://github.com/FriendsOfSymfony/FOSUserBundle -.. _`composer require`: https://getcomposer.org/doc/03-cli.md#require -.. _`symfony-bundle topic on GitHub`: https://github.com/search?q=topic%3Asymfony-bundle&type=Repositories diff --git a/bundles/override.rst b/bundles/override.rst index e90db9b9315..09d113bd88a 100644 --- a/bundles/override.rst +++ b/bundles/override.rst @@ -4,33 +4,72 @@ How to Override any Part of a Bundle ==================================== -This document is a quick reference for how to override different parts of -third-party bundles without using :doc:`/bundles/inheritance`, which is -deprecated since Symfony 3.4. +When using a third-party bundle, you might want to customize or override some of +its features. This document describes ways of overriding the most common +features of a bundle. .. tip:: The bundle overriding mechanism means that you cannot use physical paths to refer to bundle's resources (e.g. ``__DIR__/config/services.xml``). Always - use logical paths in your bundles (e.g. ``@AppBundle/Resources/config/services.xml``) + use logical paths in your bundles (e.g. ``@FooBundle/Resources/config/services.xml``) and call the :ref:`locateResource() method ` to turn them into physical paths when needed. +.. _override-templates: + Templates --------- -See :doc:`/templating/overriding`. +Third-party bundle templates can be overridden in the +``/templates/bundles//`` directory. The new templates +must use the same name and path (relative to ``/Resources/views/``) as +the original templates. + +For example, to override the ``Resources/views/Registration/confirmed.html.twig`` +template from the FOSUserBundle, create this template: +``/templates/bundles/FOSUserBundle/Registration/confirmed.html.twig`` + +.. caution:: + + If you add a template in a new location, you *may* need to clear your + cache (``php bin/console cache:clear``), even if you are in debug mode. + +Instead of overriding an entire template, you may just want to override one or +more blocks. However, since you are overriding the template you want to extend +from, you would end up in an infinite loop error. The solution is to use the +special ``!`` prefix in the template name to tell Symfony that you want to +extend from the original template, not from the overridden one: + +.. code-block:: twig + + {# templates/bundles/FOSUserBundle/Registration/confirmed.html.twig #} + {# the special '!' prefix avoids errors when extending from an overridden template #} + {% extends "@!FOSUser/Registration/confirmed.html.twig" %} + + {% block some_block %} + ... + {% endblock %} + +.. _templating-overriding-core-templates: + +.. tip:: + + Symfony internals use some bundles too, so you can apply the same technique + to override the core Symfony templates. For example, you can + :doc:`customize error pages ` overriding TwigBundle + templates. 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``). +in your application (e.g. ``config/routes.yaml``). 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, copy that -routing file into your application, modify it, and import it instead. +all. Instead of importing a third-party bundle's routing, copy +that routing file into your application, modify it, and import it instead. Controllers ----------- @@ -86,7 +125,7 @@ to a new validation group: .. code-block:: yaml - # src/Acme/UserBundle/Resources/config/validation.yml + # config/validator/validation.yaml FOS\UserBundle\Model\User: properties: plainPassword: @@ -99,7 +138,7 @@ to a new validation group: .. code-block:: xml - + `. +Translations are not related to bundles, but to translation domains. +For this reason, you can override any bundle translation file from the main +``translations/`` directory, as long as the new file uses the same domain. + +For example, to override the translations defined in the +``Resources/translations/FOSUserBundle.es.yml`` file of the FOSUserBundle, +create a ``/translations/FOSUserBundle.es.yml`` file. .. _`the Doctrine documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#overrides diff --git a/bundles/prepend_extension.rst b/bundles/prepend_extension.rst index 923796fc42e..f742df5ec69 100644 --- a/bundles/prepend_extension.rst +++ b/bundles/prepend_extension.rst @@ -12,11 +12,10 @@ 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. -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 had been written explicitly by -the user in the application configuration. +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 ``config/*`` files to prepend settings just as if +they had 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 @@ -50,7 +49,7 @@ prepend settings to a bundle extension developers can use the :method:`Symfony\\Component\\DependencyInjection\\ContainerBuilder::prependExtensionConfig` method on the :class:`Symfony\\Component\\DependencyInjection\\ContainerBuilder` instance. As this method only prepends settings, any other settings done explicitly -inside the ``app/config/config.yml`` would override these prepended settings. +inside the ``config/*`` files 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 @@ -73,7 +72,7 @@ in case a specific other bundle is not registered:: // acme_something and acme_other // // note that if the user manually configured - // use_acme_goodbye to true in app/config/config.yml + // use_acme_goodbye to true in config/services.yaml // then the setting would in the end be true and not false $container->prependExtensionConfig($name, $config); break; @@ -96,14 +95,15 @@ in case a specific other bundle is not registered:: } 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``: +``config/packages/acme_something.yaml`` in case AcmeGoodbyeBundle is not +registered and the ``entity_manager_name`` setting for ``acme_hello`` is set to +``non_default``: .. configuration-block:: .. code-block:: yaml - # app/config/config.yml + # config/packages/acme_something.yaml acme_something: # ... use_acme_goodbye: false @@ -115,7 +115,7 @@ The above would be the equivalent of writing the following into the .. code-block:: xml - + loadFromExtension('acme_something', [ // ... 'use_acme_goodbye' => false, diff --git a/bundles/remove.rst b/bundles/remove.rst deleted file mode 100644 index 0c27eacb68b..00000000000 --- a/bundles/remove.rst +++ /dev/null @@ -1,96 +0,0 @@ -.. index:: - single: Bundle; Removing a bundle - -How to Remove a 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 will likely be found in -the ``$bundles`` array declaration or added to it in a later statement if the -bundle is only registered in the development environment:: - - // app/AppKernel.php - - // ... - class AppKernel extends Kernel - { - public function registerBundles() - { - $bundles = [ - new Acme\DemoBundle\AcmeDemoBundle(), - ]; - - if (in_array($this->getEnvironment(), ['dev', 'test'])) { - // comment or remove this line: - // $bundles[] = new Acme\DemoBundle\AcmeDemoBundle(); - // ... - } - } - } - -2. Remove Bundle Configuration ------------------------------- - -Now that Symfony doesn't know about the bundle, you need to remove any -configuration and routing configuration inside the ``app/config`` directory -that refers to the bundle. - -2.1 Remove Bundle Routing -~~~~~~~~~~~~~~~~~~~~~~~~~ - -*Some* bundles require you to import routing configuration. Check for references -to the bundle in ``app/config/routing.yml`` and ``app/config/routing_dev.yml``. -If you find any references, remove them completely. - -2.2 Remove Bundle Configuration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Some bundles contain configuration in one of the ``app/config/config*.yml`` -files. Be sure to remove the related configuration from these files. You can -quickly spot bundle configuration by looking for an ``acme_demo`` (or whatever -the name of the bundle is, e.g. ``fos_user`` for the FOSUserBundle) string in -the configuration files. - -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 will be located in -`src/` for example the ``src/Acme/DemoBundle`` directory. You should remove this -directory, and any parent directories that are now empty (e.g. ``src/Acme/``). - -.. tip:: - - If you don't know the location of a bundle, you can use the - :method:`Symfony\\Component\\HttpKernel\\Bundle\\BundleInterface::getPath` method - to get the path of the bundle:: - - dump($this->container->get('kernel')->getBundle('AcmeDemoBundle')->getPath()); - die(); - -3.1 Remove Bundle Assets -~~~~~~~~~~~~~~~~~~~~~~~~ - -Remove the assets of the bundle in the web/ directory (e.g. -``web/bundles/acmedemo`` for the AcmeDemoBundle). - -4. Remove Integration in other Bundles --------------------------------------- - -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. - -.. tip:: - - 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). - -.. tip:: - - 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. diff --git a/cache.rst b/cache.rst index 9912b879e93..6301cffcd39 100644 --- a/cache.rst +++ b/cache.rst @@ -10,19 +10,25 @@ developed for high performance. The following example shows a typical usage of the cache:: - if (!$cache->has('my_cache_key')) { + use Symfony\Contracts\Cache\ItemInterface; + + // The callable will only be executed on a cache miss. + $value = $pool->get('my_cache_key', function (ItemInterface $item) { + $item->expiresAfter(3600); + // ... do some HTTP request or heavy computations - $cache->set('my_cache_key', 'foobar', 3600); - } + $computedValue = 'foobar'; - echo $cache->get('my_cache_key'); // 'foobar' + return $computedValue; + }); - // ... and to remove the cache key - $cache->delete('my_cache_key'); + echo $value; // 'foobar' + // ... and to remove the cache key + $pool->delete('my_cache_key'); -Symfony supports PSR-6 and PSR-16 cache interfaces. You can read more about -these at the :doc:`component documentation `. +Symfony supports the Cache Contracts, PSR-6/16 and Doctrine Cache interfaces. +You can read more about these at the :doc:`component documentation `. .. _cache-configuration-with-frameworkbundle: @@ -51,7 +57,7 @@ adapter (template) they use by using the ``app`` and ``system`` key like: .. code-block:: yaml - # app/config/config.yml + # config/packages/cache.yaml framework: cache: app: cache.adapter.filesystem @@ -59,7 +65,7 @@ adapter (template) they use by using the ``app`` and ``system`` key like: .. code-block:: xml - + loadFromExtension('framework', [ 'cache' => [ 'app' => 'cache.adapter.filesystem', @@ -84,23 +90,16 @@ adapter (template) they use by using the ``app`` and ``system`` key like: ], ]); -The Cache component comes with a series of adapters already created: +The Cache component comes with a series of adapters pre-configured: * :doc:`cache.adapter.apcu ` +* :doc:`cache.adapter.array ` * :doc:`cache.adapter.doctrine ` * :doc:`cache.adapter.filesystem ` * :doc:`cache.adapter.memcached ` * :doc:`cache.adapter.pdo ` +* :doc:`cache.adapter.psr6 ` * :doc:`cache.adapter.redis ` -* :doc:`PHPFileAdapter ` -* :doc:`PHPArrayAdapter ` - -* :doc:`ChainAdapter ` -* :doc:`ProxyAdapter ` -* ``cache.adapter.psr6`` - -* ``cache.adapter.system`` -* ``NullAdapter`` Some of these adapters could be configured via shortcuts. Using these shortcuts will create pool with service id of ``cache.[type]`` @@ -109,7 +108,7 @@ will create pool with service id of ``cache.[type]`` .. code-block:: yaml - # app/config/config.yml + # config/packages/cache.yaml framework: cache: directory: '%kernel.cache_dir%/pools' # Only used with cache.adapter.filesystem @@ -127,7 +126,7 @@ will create pool with service id of ``cache.[type]`` .. code-block:: xml - + loadFromExtension('framework', [ 'cache' => [ // Only used with cache.adapter.filesystem @@ -174,31 +173,50 @@ will create pool with service id of ``cache.[type]`` ], ]); -Creating Custom Pools ---------------------- +Creating Custom (Namespaced) Pools +---------------------------------- -You can also create more customized pools. All you need is an adapter: +You can also create more customized pools: .. configuration-block:: .. code-block:: yaml - # app/config/config.yml + # config/packages/cache.yaml framework: cache: default_memcached_provider: 'memcached://localhost' + pools: + # creates a "custom_thing.cache" service + # autowireable via "CacheInterface $customThingCache" + # uses the "app" cache configuration + custom_thing.cache: + adapter: cache.app + + # creates a "my_cache_pool" service + # autowireable via "CacheInterface $myCachePool" my_cache_pool: - adapter: cache.adapter.filesystem - cache.acme: + adapter: cache.adapter.array + + # uses the default_memcached_provider from above + acme.cache: adapter: cache.adapter.memcached - cache.foobar: + + # control adapter's configuration + foobar.cache: adapter: cache.adapter.memcached provider: 'memcached://user:password@example.com' + # uses the "foobar.cache" pool as its backend but controls + # the lifetime and (like all pools) has a separate cache namespace + short_cache: + adapter: foobar.cache + default_lifetime: 60 + .. code-block:: xml - + - - - + + + + + .. code-block:: php - // app/config/config.php + // config/packages/cache.php $container->loadFromExtension('framework', [ 'cache' => [ 'default_memcached_provider' => 'memcached://localhost', 'pools' => [ - 'my_cache_pool' => [ - 'adapter' => 'cache.adapter.filesystem', + 'custom_thing.cache' => [ + 'adapter' => 'cache.app', ], - 'cache.acme' => [ - 'adapter' => 'cache.adapter.memcached', + 'my_cache_pool' => [ + 'adapter' => 'cache.adapter.array', ], - 'cache.foobar' => [ + 'acme.cache' => [ 'adapter' => 'cache.adapter.memcached', - 'provider' => 'memcached://user:password@example.com', ], - ], - ], - ]); - - -The configuration above will create 3 services: ``my_cache_pool``, ``cache.acme`` -and ``cache.foobar``. The ``my_cache_pool`` pool is using the ArrayAdapter -and the other two are using the :doc:`MemcachedAdapter `. -The ``cache.acme`` pool is using the Memcached server on localhost and ``cache.foobar`` -is using the Memcached server at example.com. - -For advanced configurations it could sometimes be useful to use a pool as an adapter. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - cache: - app: my_configured_app_cache - pools: - my_cache_pool: - adapter: cache.adapter.memcached - provider: 'memcached://user:password@example.com' - cache.short_cache: - adapter: my_cache_pool - default_lifetime: 60 - cache.long_cache: - adapter: my_cache_pool - default_lifetime: 604800 - my_configured_app_cache: - # "cache.adapter.filesystem" is the default for "cache.app" - adapter: cache.adapter.filesystem - default_lifetime: 3600 - - .. code-block:: xml - - - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', [ - 'cache' => [ - 'app' => 'my_configured_app_cache', - 'pools' => [ - 'my_cache_pool' => [ + 'foobar.cache' => [ 'adapter' => 'cache.adapter.memcached', 'provider' => 'memcached://user:password@example.com', ], - 'cache.short_cache' => [ - 'adapter' => 'cache.adapter.memcached', + 'short_cache' => [ + 'adapter' => 'foobar.cache', 'default_lifetime' => 60, ], - 'cache.long_cache' => [ - 'adapter' => 'cache.adapter.memcached', - 'default_lifetime' => 604800, - ], - 'my_configured_app_cache' => [ - // "cache.adapter.filesystem" is the default for "cache.app" - 'adapter' => 'cache.adapter.filesystem', - 'default_lifetime' => 3600, - ], ], ], ]); +Each pool manages a set of independent cache keys: keys of different pools +*never* collide, even if they share the same backend. This is achieved by prefixing +keys with a namespace that's generated by hashing the name of the pool, the name +of the compiled container class and a :ref:`configurable seed` +that defaults to the project directory. + +Each custom pool becomes a service where the service id is the name of the pool +(e.g. ``custom_thing.cache``). An autowiring alias is also created for each pool +using the camel case version of its name - e.g. ``custom_thing.cache`` can be +injected automatically by naming the argument ``$customThingCache`` and type-hinting it +with either :class:`Symfony\\Contracts\\Cache\\CacheInterface` or +``Psr\Cache\CacheItemPoolInterface``:: + + use Symfony\Contracts\Cache\CacheInterface; + + // from a controller method + public function listProducts(CacheInterface $customThingCache) + { + // ... + } + + // in a service + public function __construct(CacheInterface $customThingCache) + { + // ... + } + Custom Provider Options ----------------------- @@ -330,7 +303,7 @@ and use that when configuring the pool. .. code-block:: yaml - # app/config/config.yml + # config/packages/cache.yaml framework: cache: pools: @@ -348,7 +321,7 @@ and use that when configuring the pool. .. code-block:: xml - + loadFromExtension('framework', [ 'cache' => [ 'pools' => [ @@ -417,32 +390,114 @@ Symfony stores the item automatically in all the missing pools. .. code-block:: yaml - # app/config/config.yml + # config/packages/cache.yaml framework: cache: pools: my_cache_pool: - adapter: cache.adapter.psr6 - provider: app.my_cache_chain_adapter - cache.my_redis: - adapter: cache.adapter.redis - provider: 'redis://user:password@example.com' - cache.apcu: - adapter: cache.adapter.apcu - cache.array: - adapter: cache.adapter.filesystem + default_lifetime: 31536000 # One year + adapters: + - cache.adapter.array + - cache.adapter.apcu + - {name: cache.adapter.redis, provider: 'redis://user:password@example.com'} + .. code-block:: xml - services: - app.my_cache_chain_adapter: - class: Symfony\Component\Cache\Adapter\ChainAdapter - arguments: - - ['@cache.array', '@cache.apcu', '@cache.my_redis'] - - 31536000 # One year + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/cache.php + $container->loadFromExtension('framework', [ + 'cache' => [ + 'pools' => [ + 'my_cache_pool' => [ + 'default_lifetime' => 31536000, // One year + 'adapters' => [ + 'cache.adapter.array', + 'cache.adapter.apcu', + ['name' => 'cache.adapter.redis', 'provider' => 'redis://user:password@example.com'], + ], + ], + ], + ], + ]); + +Using Cache Tags +---------------- + +In applications with many cache keys it could be useful to organize the data stored +to be able to invalidate the cache more efficient. One way to achieve that is to +use cache tags. One or more tags could be added to the cache item. All items with +the same key could be invalidate with one function call:: + + use Symfony\Contracts\Cache\ItemInterface; + use Symfony\Contracts\Cache\TagAwareCacheInterface; + + class SomeClass + { + private $myCachePool; + + // using autowiring to inject the cache pool + public function __construct(TagAwareCacheInterface $myCachePool) + { + $this->myCachePool = $myCachePool; + } + + public function someMethod() + { + $value0 = $this->myCachePool->get('item_0', function (ItemInterface $item) { + $item->tag(['foo', 'bar']); + + return 'debug'; + }); + + $value1 = $this->myCachePool->get('item_1', function (ItemInterface $item) { + $item->tag('foo'); + + return 'debug'; + }); + + // Remove all cache keys tagged with "bar" + $this->myCachePool->invalidateTags(['bar']); + } + } + +The cache adapter needs to implement :class:`Symfony\\Contracts\\Cache\\TagAwareCacheInterface` +to enable this feature. This could be added by using the following configuration. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/cache.yaml + framework: + cache: + pools: + my_cache_pool: + adapter: cache.adapter.redis + tags: true .. code-block:: xml - + - - - - + - - - - - - - - - 31536000 - - .. code-block:: php - // app/config/config.php + // config/packages/cache.php $container->loadFromExtension('framework', [ 'cache' => [ 'pools' => [ 'my_cache_pool' => [ - 'adapter' => 'cache.adapter.psr6', - 'provider' => 'app.my_cache_chain_adapter', + 'adapter' => 'cache.adapter.redis', + 'tags' => true, ], - 'cache.my_redis' => [ + ], + ], + ]); + +Tags are stored in the same pool by default. This is good in most scenarios. But +sometimes it might be better to store the tags in a different pool. That could be +achieved by specifying the adapter. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/cache.yaml + framework: + cache: + pools: + my_cache_pool: + adapter: cache.adapter.redis + tags: tag_pool + tag_pool: + adapter: cache.adapter.apcu + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/cache.php + $container->loadFromExtension('framework', [ + 'cache' => [ + 'pools' => [ + 'my_cache_pool' => [ 'adapter' => 'cache.adapter.redis', - 'provider' => 'redis://user:password@example.com', + 'tags' => 'tag_pool', ], - 'cache.apcu' => [ + 'tag_pool' => [ 'adapter' => 'cache.adapter.apcu', ], - 'cache.array' => [ - 'adapter' => 'cache.adapter.filesystem', - ], ], ], ]); - $container->getDefinition('app.my_cache_chain_adapter', \Symfony\Component\Cache\Adapter\ChainAdapter::class) - ->addArgument([ - new Reference('cache.array'), - new Reference('cache.apcu'), - new Reference('cache.my_redis'), - ]) - ->addArgument(31536000); - .. note:: - In this configuration the ``my_cache_pool`` pool is using the ``cache.adapter.psr6`` - adapter and the ``app.my_cache_chain_adapter`` service as a provider. That is - because ``ChainAdapter`` does not support the ``cache.pool`` tag. So it is decorated - with the ``ProxyAdapter``. - + The interface :class:`Symfony\\Contracts\\Cache\\TagAwareCacheInterface` is + autowired to the ``cache.app`` service. Clearing the Cache ------------------ @@ -527,6 +600,12 @@ The global clearer clears all the cache in every pool. The system cache clearer is used in the ``bin/console cache:clear`` command. The app clearer is the default clearer. +To see all available cache pools: + +.. code-block:: terminal + + $ php bin/console cache:pool:list + Clear one pool: .. code-block:: terminal diff --git a/components/asset.rst b/components/asset.rst index a2e1bf69327..71b2aa13027 100644 --- a/components/asset.rst +++ b/components/asset.rst @@ -44,7 +44,7 @@ Installation .. code-block:: terminal - $ composer require symfony/asset:^3.4 + $ composer require symfony/asset .. include:: /components/require_autoload.rst.inc @@ -281,8 +281,8 @@ You can also pass a schema-agnostic URL:: // result: //static.example.com/images/logo.png?v1 This is useful because assets will automatically be requested via HTTPS if -a visitor is viewing your site in https. Just make sure that your CDN host -supports https. +a visitor is viewing your site in https. If you want to use this, make sure +that your CDN host supports HTTPS. In case you serve assets from more than one domain to improve application performance, pass an array of URLs as the first argument to the ``UrlPackage`` @@ -371,6 +371,32 @@ document inside a template:: echo $packages->getUrl('resume.pdf', 'doc'); // result: /somewhere/deep/for/documents/resume.pdf?v1 +Local Files and Other Protocols +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to HTTP this component supports other protocols (such as ``file://`` +and ``ftp://``). This allows for example to serve local files in order to +improve performance:: + + use Symfony\Component\Asset\UrlPackage; + // ... + + $localPackage = new UrlPackage( + 'file:///path/to/images/', + new EmptyVersionStrategy() + ); + + $ftpPackage = new UrlPackage( + 'ftp://example.com/images/', + new EmptyVersionStrategy() + ); + + echo $localPackage->getUrl('/logo.png'); + // result: file:///path/to/images/logo.png + + echo $ftpPackage->getUrl('/logo.png'); + // result: ftp://example.com/images/logo.png + Learn more ---------- diff --git a/components/browser_kit.rst b/components/browser_kit.rst index ba295db0eb4..bfeab500f46 100644 --- a/components/browser_kit.rst +++ b/components/browser_kit.rst @@ -10,16 +10,17 @@ The BrowserKit Component .. note:: - The BrowserKit component can only make internal requests to your application. - If you need to make requests to external sites and applications, consider - using `Goutte`_, a simple web scraper based on Symfony Components. + In Symfony versions prior to 4.3, the BrowserKit component could only make + internal requests to your application. Starting from Symfony 4.3, this + component can also :ref:`make HTTP requests to any public site ` + when using it in combination with the :doc:`HttpClient component `. Installation ------------ .. code-block:: terminal - $ composer require symfony/browser-kit:^3.4 + $ composer require symfony/browser-kit .. include:: /components/require_autoload.rst.inc @@ -58,9 +59,10 @@ This method accepts a request and should return a response:: } For a simple implementation of a browser based on the HTTP layer, have a look -at `Goutte`_. For an implementation based on ``HttpKernelInterface``, have -a look at the :class:`Symfony\\Component\\HttpKernel\\Client` provided by -the :doc:`HttpKernel component `. +at the :class:`Symfony\\Component\\BrowserKit\\HttpBrowser` provided by +:ref:`this component `. For an implementation based +on ``HttpKernelInterface``, have a look at the :class:`Symfony\\Component\\HttpKernel\\Client` +provided by the :doc:`HttpKernel component `. Making Requests ~~~~~~~~~~~~~~~ @@ -79,17 +81,35 @@ The value returned by the ``request()`` method is an instance of the :doc:`DomCrawler component `, which allows accessing and traversing HTML elements programmatically. +The :method:`Symfony\\Component\\BrowserKit\\Client::xmlHttpRequest` method, +which defines the same arguments as the ``request()`` method, is a shortcut to +make AJAX requests:: + + use Acme\Client; + + $client = new Client(); + // the required HTTP_X_REQUESTED_WITH header is added automatically + $crawler = $client->xmlHttpRequest('GET', '/'); + Clicking Links ~~~~~~~~~~~~~~ -The ``Crawler`` object is capable of simulating link clicks. First, pass the -text content of the link to the ``selectLink()`` method, which returns a -``Link`` object. Then, pass this object to the ``click()`` method, which -performs the needed HTTP GET request to simulate the link click:: +The ``Client`` object is capable of simulating link clicks. Pass the text +content of the link and the client will perform the needed HTTP GET request to +simulate the link click:: use Acme\Client; $client = new Client(); + $client->request('GET', '/product/123'); + + $crawler = $client->clickLink('Go elsewhere...'); + +If you need the :class:`Symfony\\Component\\DomCrawler\\Link` object that +provides access to the link properties (e.g. ``$link->getMethod()``, +``$link->getUri()``), use this other method:: + + // ... $crawler = $client->request('GET', '/product/123'); $link = $crawler->selectLink('Go elsewhere...')->link(); $client->click($link); @@ -97,27 +117,48 @@ performs the needed HTTP GET request to simulate the link click:: Submitting Forms ~~~~~~~~~~~~~~~~ -The ``Crawler`` object is also capable of selecting forms. First, select any of -the form's buttons with the ``selectButton()`` method. Then, use the ``form()`` -method to select the form which the button belongs to. - -After selecting the form, fill in its data and send it using the ``submit()`` -method (which makes the needed HTTP POST request to submit the form contents):: +The ``Client`` object is also capable of submitting forms. First, select the +form using any of its buttons and then override any of its properties (method, +field values, etc.) before submitting it:: use Acme\Client; - // make a real request to an external site $client = new Client(); $crawler = $client->request('GET', 'https://github.com/login'); + // find the form with the 'Log in' button and submit it + // 'Log in' can be the text content, id, value or name of a - {{ form_end(form) }} - -See :doc:`/form/form_customization` for more details. - -Update your Database Schema ---------------------------- - -If you've updated the ``User`` entity during this tutorial, you have to update -your database schema using this command: - -.. code-block:: terminal - - $ php bin/console doctrine:schema:update --force - -That's it! Head to ``/register`` to try things out! - -.. _registration-form-via-email: - -Having a Registration form with only Email (no Username) --------------------------------------------------------- - -If you want your users to login via email and you don't need a username, then you -can remove it from your ``User`` entity entirely. Instead, make ``getUsername()`` -return the ``email`` property:: - - // src/AppBundle/Entity/User.php - // ... - - class User implements UserInterface - { - // ... - - public function getUsername() - { - return $this->email; - } - - // ... - } - -Next, just update the ``providers`` section of your ``security.yml`` file -so that Symfony knows how to load your users via the ``email`` property on -login. See :ref:`authenticating-someone-with-a-custom-entity-provider`. - -Adding a "accept terms" Checkbox --------------------------------- - -Sometimes, you want a "Do you accept the terms and conditions" checkbox on your -registration form. The only trick is that you want to add this field to your form -without adding an unnecessary new ``termsAccepted`` property to your ``User`` entity -that you'll never need. - -To do this, add a ``termsAccepted`` field to your form, but set its -:ref:`mapped ` option to ``false``:: - - // src/AppBundle/Form/UserType.php - // ... - - use Symfony\Component\Form\Extension\Core\Type\CheckboxType; - use Symfony\Component\Form\Extension\Core\Type\EmailType; - use Symfony\Component\Validator\Constraints\IsTrue; - - class UserType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('email', EmailType::class) - // ... - ->add('termsAccepted', CheckboxType::class, [ - 'mapped' => false, - 'constraints' => new IsTrue(), - ]) - ; - } - } - -The :ref:`constraints ` option is also used, which allows -us to add validation, even though there is no ``termsAccepted`` property on ``User``. - -.. _`CVE-2013-5750`: https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form -.. _`FOSUserBundle`: https://github.com/FriendsOfSymfony/FOSUserBundle -.. _`bcrypt`: https://en.wikipedia.org/wiki/Bcrypt +.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html diff --git a/doctrine/repository.rst b/doctrine/repository.rst deleted file mode 100644 index 5428c71bb95..00000000000 --- a/doctrine/repository.rst +++ /dev/null @@ -1,100 +0,0 @@ -.. index:: - single: Doctrine; Custom Repository Class - -How to Create custom Repository Classes -======================================= - -Constructing and using complex queries inside controllers complicate the -maintenance of your application. In order to isolate, reuse and test these -queries, it's a good practice to create a custom repository class for your -entity. Methods containing your query logic can then be stored in this class. - -To do this, add the repository class name to your entity's mapping definition: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Entity/Product.php - namespace AppBundle\Entity; - - use Doctrine\ORM\Mapping as ORM; - - /** - * @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository") - */ - class Product - { - // ... - } - - .. code-block:: yaml - - # src/AppBundle/Resources/config/doctrine/Product.orm.yml - AppBundle\Entity\Product: - type: entity - repositoryClass: AppBundle\Repository\ProductRepository - # ... - - .. code-block:: xml - - - - - - - - - - - -Then, create an empty ``AppBundle\Repository\ProductRepository`` class extending -from ``Doctrine\ORM\EntityRepository``. - -Next, add a new method - ``findAllOrderedByName()`` - to the newly-generated -``ProductRepository`` class. This method will query for all the ``Product`` -entities, ordered alphabetically by name:: - - // src/AppBundle/Repository/ProductRepository.php - namespace AppBundle\Repository; - - 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(); - } - } - -.. tip:: - - 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:: - - use AppBundle\Entity\Product; - // ... - - public function listAction() - { - $products = $this->getDoctrine() - ->getRepository(Product::class) - ->findAllOrderedByName(); - } - -.. note:: - - When using a custom repository class, you still have access to the default - finder methods such as ``find()`` and ``findAll()``. diff --git a/doctrine/resolve_target_entity.rst b/doctrine/resolve_target_entity.rst index 543f7711252..a60dfe3e604 100644 --- a/doctrine/resolve_target_entity.rst +++ b/doctrine/resolve_target_entity.rst @@ -39,8 +39,8 @@ brevity) to explain how to set up and use the ``ResolveTargetEntityListener``. A Customer entity:: - // src/AppBundle/Entity/Customer.php - namespace AppBundle\Entity; + // src/Entity/Customer.php + namespace App\Entity; use Acme\CustomerBundle\Entity\Customer as BaseCustomer; use Acme\InvoiceBundle\Model\InvoiceSubjectInterface; @@ -109,17 +109,17 @@ about the replacement: .. code-block:: yaml - # app/config/config.yml + # config/packages/doctrine.yaml doctrine: # ... orm: # ... resolve_target_entities: - Acme\InvoiceBundle\Model\InvoiceSubjectInterface: AppBundle\Entity\Customer + Acme\InvoiceBundle\Model\InvoiceSubjectInterface: App\Entity\Customer .. code-block:: xml - + - AppBundle\Entity\Customer + App\Entity\Customer .. code-block:: php - // app/config/config.php + // config/packages/doctrine.php use Acme\InvoiceBundle\Model\InvoiceSubjectInterface; - use AppBundle\Entity\Customer; + use App\Entity\Customer; $container->loadFromExtension('doctrine', [ 'orm' => [ diff --git a/doctrine/reverse_engineering.rst b/doctrine/reverse_engineering.rst index 0a48e98c7d2..1087e8d64f3 100644 --- a/doctrine/reverse_engineering.rst +++ b/doctrine/reverse_engineering.rst @@ -47,8 +47,7 @@ to a post record thanks to a foreign key constraint. ) 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). +correctly setup in the ``.env`` file (or ``.env.local`` override file). The first step towards building entity classes from an existing database is to ask Doctrine to introspect the database and generate the corresponding @@ -57,115 +56,58 @@ table fields. .. code-block:: terminal - $ php bin/console doctrine:mapping:import --force AppBundle xml + $ php bin/console doctrine:mapping:import "App\Entity" annotation --path=src/Entity This command line tool asks Doctrine to introspect the database and generate -the XML metadata files under the ``src/AppBundle/Resources/config/doctrine`` -folder of your bundle. This generates two files: ``BlogPost.orm.xml`` and -``BlogComment.orm.xml``. +new PHP classes with annotation metadata into ``src/Entity``. This generates two +files: ``BlogPost.php`` and ``BlogComment.php``. .. tip:: - It's also possible to generate the metadata files in YAML format by changing - the last argument to ``yml``. + It's also possible to generate the metadata files into XML or eventually into YAML: -The generated ``BlogPost.orm.xml`` metadata file looks as follows: + .. code-block:: terminal -.. code-block:: xml + $ php bin/console doctrine:mapping:import "App\Entity" xml --path=config/doctrine - - - - - - - - - - - + In this case, make sure to adapt your mapping configuration accordingly: -Once the metadata files are generated, you can ask Doctrine to build related -entity classes by executing the following command. + .. code-block:: yaml + + # config/packages/doctrine.yaml + doctrine: + # ... + orm: + # ... + mappings: + App: + is_bundle: false + type: xml # "yml" is marked as deprecated for doctrine v2.6+ and will be removed in v3 + dir: '%kernel.project_dir%/config/doctrine' + prefix: 'App\Entity' + alias: App + +Generating the Getters & Setters or PHP Classes +----------------------------------------------- + +The generated PHP classes now have properties and annotation metadata, but they +do *not* have any getter or setter methods. If you generated XML or YAML metadata, +you don't even have the PHP classes! + +To generate the missing getter/setter methods (or to *create* the classes if necessary), +run: .. code-block:: terminal - // generates entity classes with annotation mappings - $ php bin/console doctrine:mapping:convert annotation ./src - -.. caution:: - - If you want to use annotations, you must remove the XML (or YAML) files - after running this command. This is necessary as - :ref:`it is not possible to mix mapping configuration formats ` - -For example, the newly created ``BlogComment`` entity class looks as follow:: - - // src/AppBundle/Entity/BlogComment.php - namespace AppBundle\Entity; - - use Doctrine\ORM\Mapping as ORM; - - /** - * @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. + // generates getter/setter methods + $ php bin/console make:entity --regenerate App .. note:: - 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. + If you want to have a OneToMany relationship, you will need to add + it manually into the entity (e.g. add a ``comments`` property to ``BlogPost``) + 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! diff --git a/email.rst b/email.rst index 0e60f8b520f..82c07d06f36 100644 --- a/email.rst +++ b/email.rst @@ -1,115 +1,79 @@ .. index:: single: Emails -How to Send an Email -==================== +Swift Mailer +============ -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. +.. note:: -.. _swift-mailer-configuration: + In Symfony 4.3, the :doc:`Mailer ` component was introduced and can + be used instead of Swift Mailer. -Configuration -------------- +Symfony provides a mailer feature based on the popular `Swift Mailer`_ library +via the `SwiftMailerBundle`_. This mailer supports sending messages with your +own mail servers as well as using popular email providers like `Mandrill`_, +`SendGrid`_, and `Amazon SES`_. -To use Swift Mailer, you'll need to configure it for your mail server. - -.. tip:: +Installation +------------ - 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 applications using :ref:`Symfony Flex `, run this command to +install the Swift Mailer based mailer before using it: -In a standard Symfony installation, some ``swiftmailer`` configuration is -already included: - -.. configuration-block:: - - .. code-block:: yaml +.. code-block:: terminal - # app/config/config.yml - swiftmailer: - transport: '%mailer_transport%' - host: '%mailer_host%' - username: '%mailer_user%' - password: '%mailer_password%' + $ composer require symfony/swiftmailer-bundle - .. code-block:: xml +If your application doesn't use Symfony Flex, follow the installation +instructions on `SwiftMailerBundle`_. - - - - - - +.. _swift-mailer-configuration: - .. code-block:: php +Configuration +------------- - // app/config/config.php - $container->loadFromExtension('swiftmailer', [ - 'transport' => "%mailer_transport%", - 'host' => "%mailer_host%", - 'username' => "%mailer_user%", - 'password' => "%mailer_password%", - ]); +The ``config/packages/swiftmailer.yaml`` file that's created when installing the +mailer provides all the initial config needed to send emails, except your mail +server connection details. Those parameters are defined in the ``MAILER_URL`` +environment variable in the ``.env`` file: -These values (e.g. ``%mailer_transport%``), are reading from the parameters -that are set in the :ref:`parameters.yml ` file. You -can modify the values in that file, or set the values directly here. +.. code-block:: bash -The following configuration attributes are available: + # .env (or override MAILER_URL in .env.local to avoid committing your changes) -* ``transport`` (``smtp``, ``mail``, ``sendmail``, or ``gmail``) -* ``username`` -* ``password`` -* ``host`` -* ``port`` -* ``encryption`` (``tls``, or ``ssl``) -* ``auth_mode`` (``plain``, ``login``, or ``cram-md5``) -* ``spool`` + # use this to disable email delivery + MAILER_URL=null://localhost - * ``type`` (how to queue the messages, ``file`` or ``memory`` is supported, see :doc:`/email/spool`) - * ``path`` (where to store the messages) -* ``delivery_addresses`` (an array of email addresses where to send ALL emails) -* ``disable_delivery`` (set to true to disable delivery completely) + # use this to configure a traditional SMTP server + MAILER_URL=smtp://localhost:465?encryption=ssl&auth_mode=login&username=&password= .. caution:: - Starting from SwiftMailer 5.4.5, the ``mail`` transport is deprecated - and will be removed in version 6. Consider using another transport like - ``smtp``, ``sendmail`` or ``gmail``. + If the username, password or host contain any character considered special in a + URI (such as ``+``, ``@``, ``$``, ``#``, ``/``, ``:``, ``*``, ``!``), you must + encode them. See `RFC 3986`_ for the full list of reserved characters or use the + :phpfunction:`urlencode` function to encode them. + +Refer to the :doc:`SwiftMailer configuration reference ` +for the detailed explanation of all the available config options. 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:: +of the message and is accessible via the ``Swift_Mailer`` service. Overall, +sending an email is pretty straightforward:: - public function indexAction($name, \Swift_Mailer $mailer) + public function index($name, \Swift_Mailer $mailer) { $message = (new \Swift_Message('Hello Email')) ->setFrom('send@example.com') ->setTo('recipient@example.com') ->setBody( $this->renderView( - // app/Resources/views/Emails/registration.html.twig - 'Emails/registration.html.twig', + // templates/emails/registration.html.twig + 'emails/registration.html.twig', ['name' => $name] ), 'text/html' @@ -118,7 +82,8 @@ an email is pretty straightforward:: // you can remove the following code if you don't define a text version for your emails ->addPart( $this->renderView( - 'Emails/registration.txt.twig', + // templates/emails/registration.txt.twig + 'emails/registration.txt.twig', ['name' => $name] ), 'text/plain' @@ -127,9 +92,6 @@ an email is pretty straightforward:: $mailer->send($message); - // or, you can also fetch the mailer service this way - // $this->get('mailer')->send($message); - return $this->render(...); } @@ -139,7 +101,7 @@ template might look something like this: .. code-block:: html+twig - {# app/Resources/views/Emails/registration.html.twig #} + {# templates/emails/registration.html.twig #}

You did it! You registered!

Hi {{ name }}! You're successfully registered. @@ -153,20 +115,553 @@ template might look something like this: 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. +adding HTML content, and much more. Refer to the `Creating Messages`_ section +of the Swift Mailer documentation for more details. + +.. _email-using-gmail: + +Using Gmail to Send Emails +-------------------------- + +During development, you might prefer to send emails using Gmail instead of +setting up a regular SMTP server. To do that, update the ``MAILER_URL`` of your +``.env`` file to this: + +.. code-block:: bash + + # username is your full Gmail or Google Apps email address + MAILER_URL=gmail://username:password@localhost + +The ``gmail`` transport is a shortcut that uses the ``smtp`` transport, ``ssl`` +encryption, ``login`` auth mode and ``smtp.gmail.com`` host. If your app uses +other encryption or auth mode, you must override those values +(:doc:`see mailer config reference `): + +.. code-block:: bash + + # username is your full Gmail or Google Apps email address + MAILER_URL=gmail://username:password@localhost?encryption=tls&auth_mode=oauth + +If your Gmail account uses 2-Step-Verification, you must `generate an App password`_ +and use it as the value of the mailer password. You must also ensure that you +`allow less secure applications to access your Gmail account`_. + +Using Cloud Services to Send Emails +----------------------------------- + +Cloud mailing services are a popular option for companies that don't want to set +up and maintain their own reliable mail servers. To use these services in a +Symfony app, update the value of ``MAILER_URL`` in the ``.env`` +file. For example, for `Amazon SES`_ (Simple Email Service): + +.. code-block:: bash + + # The host will be different depending on your AWS zone + # The username/password credentials are obtained from the Amazon SES console + MAILER_URL=smtp://email-smtp.us-east-1.amazonaws.com:587?encryption=tls&username=YOUR_SES_USERNAME&password=YOUR_SES_PASSWORD + +Use the same technique for other mail services, as most of the time there is +nothing more to it than configuring an SMTP endpoint. + +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 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 (with optional exceptions). + +Disabling Sending +~~~~~~~~~~~~~~~~~ + +You can disable sending email by setting the ``disable_delivery`` option to +``true``, which is the default value used by Symfony in the ``test`` environment +(email messages will continue to be sent in the other environments): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/test/swiftmailer.yaml + swiftmailer: + disable_delivery: true + + .. code-block:: xml + + + + + + + + + .. code-block:: php + + // config/packages/test/swiftmailer.php + $container->loadFromExtension('swiftmailer', [ + 'disable_delivery' => "true", + ]); + +.. _sending-to-a-specified-address: + +Sending to a Specified Address(es) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can also choose to have all email sent to a specific address or a list of addresses, instead +of the address actually specified when sending the message. This can be done +via the ``delivery_addresses`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/dev/swiftmailer.yaml + swiftmailer: + delivery_addresses: ['dev@example.com'] + + .. code-block:: xml + + + + + + + dev@example.com + + + + .. code-block:: php + + // config/packages/dev/swiftmailer.php + $container->loadFromExtension('swiftmailer', [ + 'delivery_addresses' => ['dev@example.com'], + ]); + +Now, suppose you're sending an email to ``recipient@example.com`` in a controller:: + + public function index($name, \Swift_Mailer $mailer) + { + $message = (new \Swift_Message('Hello Email')) + ->setFrom('send@example.com') + ->setTo('recipient@example.com') + ->setBody( + $this->renderView( + // templates/hello/email.txt.twig + 'hello/email.txt.twig', + ['name' => $name] + ) + ) + ; + $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. + +.. note:: + + 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. + +.. _sending-to-a-specified-address-but-with-exceptions: + +Sending to a Specified Address but with Exceptions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Suppose you want to have all email redirected to a specific address, +(like in the above scenario to ``dev@example.com``). But then you may want +email sent to some specific email addresses to go through after all, and +not be redirected (even if it is in the dev environment). This can be done +by adding the ``delivery_whitelist`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/dev/swiftmailer.yaml + swiftmailer: + delivery_addresses: ['dev@example.com'] + delivery_whitelist: + # all email addresses matching these regexes will be delivered + # like normal, as well as being sent to dev@example.com + - '/@specialdomain\.com$/' + - '/^admin@mydomain\.com$/' + + .. code-block:: xml + + + + + + + + /@specialdomain\.com$/ + /^admin@mydomain\.com$/ + dev@example.com + + + + .. code-block:: php + + // config/packages/dev/swiftmailer.php + $container->loadFromExtension('swiftmailer', [ + 'delivery_addresses' => ["dev@example.com"], + 'delivery_whitelist' => [ + // all email addresses matching these regexes will be delivered + // like normal, as well as being sent to dev@example.com + '/@specialdomain\.com$/', + '/^admin@mydomain\.com$/', + ], + ]); + +In the above example all email messages will be redirected to ``dev@example.com`` +and messages sent to the ``admin@mydomain.com`` address or to any email address +belonging to the domain ``specialdomain.com`` will also be delivered as normal. + +.. caution:: + + The ``delivery_whitelist`` option is ignored unless the ``delivery_addresses`` option is defined. + +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 +``dev`` environment, which will cause the redirect to stop and allow you to open +the report with details of the sent emails. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/dev/web_profiler.yaml + web_profiler: + intercept_redirects: true + + .. code-block:: xml + + + + + + + + + .. code-block:: php + + // config/packages/dev/web_profiler.php + $container->loadFromExtension('web_profiler', [ + 'intercept_redirects' => 'true', + ]); + +.. tip:: + + 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. + +.. tip:: + + In addition to the features provided by Symfony, there are applications that + can help you test emails during application development, like `MailCatcher`_ + and `MailHog`_. + +How to Spool Emails +------------------- + +The default behavior of the Symfony mailer is to send the email messages +immediately. You may, however, want to avoid the performance hit of the +communication to the email server, 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 makes the mailer to not attempt to send the email message but instead save +it 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. + +.. _email-spool-memory: + +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 +this spool, use the following configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/swiftmailer.yaml + swiftmailer: + # ... + spool: { type: memory } + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/swiftmailer.php + $container->loadFromExtension('swiftmailer', [ + // ... + 'spool' => ['type' => 'memory'], + ]); + +.. _spool-using-a-file: + +Spool Using Files +~~~~~~~~~~~~~~~~~ + +When you use the filesystem for spooling, Symfony creates a folder in the given +path for each mail service (e.g. "default" for the default service). This folder +will contain files for each email in the spool. So make sure this directory is +writable by Symfony (or your webserver/php)! + +In order to use the spool with files, use the following configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/swiftmailer.yaml + swiftmailer: + # ... + spool: + type: file + path: /path/to/spooldir + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/swiftmailer.php + $container->loadFromExtension('swiftmailer', [ + // ... + + 'spool' => [ + 'type' => 'file', + 'path' => '/path/to/spooldir', + ], + ]); + +.. tip:: + + If you want to store the spool somewhere with your project directory, + remember that you can use the ``%kernel.project_dir%`` parameter to reference + the project's root: + + .. code-block:: yaml + + path: '%kernel.project_dir%/var/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: + +.. code-block:: terminal + + $ APP_ENV=prod php bin/console swiftmailer:spool:send + +It has an option to limit the number of messages to be sent: + +.. code-block:: terminal + + $ APP_ENV=prod php bin/console swiftmailer:spool:send --message-limit=10 + +You can also set the time limit in seconds: + +.. code-block:: terminal + + $ APP_ENV=prod php bin/console swiftmailer:spool:send --time-limit=10 + +In practice you will not want to run this manually. Instead, the console command +should be triggered by a cron job or scheduled task and run at a regular +interval. + +.. caution:: + + When you create a message with SwiftMailer, it generates a ``Swift_Message`` + class. If the ``swiftmailer`` service is lazy loaded, it generates instead a + proxy class named ``Swift_Message_``. + + If you use the memory spool, this change is transparent and has no impact. + But when using the filesystem spool, the message class is serialized in + a file with the randomized class name. The problem is that this random + class name changes on every cache clear. + + So if you send a mail and then you clear the cache, on the next execution of + ``swiftmailer:spool:send`` an error will raise because the class + ``Swift_Message_`` doesn't exist (anymore). + + The solutions are either to use the memory spool or to load the + ``swiftmailer`` service without the ``lazy`` option (see :doc:`/service_container/lazy_services`). + +How to Test that an Email is Sent in a Functional Test +------------------------------------------------------ + +Sending emails 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 :doc:`the Symfony Profiler `. + +Start with a controller action that sends an email:: + + public function sendEmail($name, \Swift_Mailer $mailer) + { + $message = (new \Swift_Message('Hello Email')) + ->setFrom('send@example.com') + ->setTo('recipient@example.com') + ->setBody('You should see me from the profiler!') + ; + + $mailer->send($message); + + // ... + } + +In your functional test, use the ``swiftmailer`` collector on the profiler +to get information about the messages sent on the previous request:: + + // tests/Controller/MailControllerTest.php + namespace App\Tests\Controller; + + use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + + class MailControllerTest extends WebTestCase + { + public function testMailIsSentAndContentIsOk() + { + $client = static::createClient(); + + // enables 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'); + + // checks that an email was sent + $this->assertSame(1, $mailCollector->getMessageCount()); + + $collectedMessages = $mailCollector->getMessages(); + $message = $collectedMessages[0]; + + // Asserting email data + $this->assertInstanceOf('Swift_Message', $message); + $this->assertSame('Hello Email', $message->getSubject()); + $this->assertSame('send@example.com', key($message->getFrom())); + $this->assertSame('recipient@example.com', key($message->getTo())); + $this->assertSame( + 'You should see me from the profiler!', + $message->getBody() + ); + } + } + +Troubleshooting +~~~~~~~~~~~~~~~ + +Problem: The Collector Object Is ``null`` +......................................... -Learn more ----------- +The email collector is only available when the profiler is enabled and collects +information, as explained in :doc:`/testing/profiling`. -.. toctree:: - :maxdepth: 1 - :glob: +Problem: The Collector Doesn't Contain the Email +................................................ - email/* +If a redirection is performed after sending the email (for example when you send +an email after a form is processed and before redirecting to another page), make +sure that the test client doesn't follow the redirects, as explained in +:doc:`/testing`. Otherwise, the collector will contain the information of the +redirected page and the email won't be accessible. +.. _`MailCatcher`: https://github.com/sj26/mailcatcher +.. _`MailHog`: https://github.com/mailhog/MailHog .. _`Swift Mailer`: http://swiftmailer.org/ +.. _`SwiftMailerBundle`: https://github.com/symfony/swiftmailer-bundle .. _`Creating Messages`: https://swiftmailer.symfony.com/docs/messages.html .. _`Mandrill`: https://mandrill.com/ .. _`SendGrid`: https://sendgrid.com/ .. _`Amazon SES`: http://aws.amazon.com/ses/ +.. _`generate an App password`: https://support.google.com/accounts/answer/185833 +.. _`allow less secure applications to access your Gmail account`: https://support.google.com/accounts/answer/6010255 +.. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt diff --git a/email/cloud.rst b/email/cloud.rst deleted file mode 100644 index 51103f1190d..00000000000 --- a/email/cloud.rst +++ /dev/null @@ -1,120 +0,0 @@ -.. index:: - single: Emails; Using the cloud - -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, -:doc:`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 article shows how to integrate `Amazon's Simple Email Service (SES)`_ -into Symfony. - -.. note:: - - 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``: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - swiftmailer: - transport: smtp - host: email-smtp.us-east-1.amazonaws.com - port: 587 # different ports are available, see SES console - encryption: tls # TLS encryption is required - username: AWS_SES_SMTP_USERNAME # to be created in the SES console - password: AWS_SES_SMTP_PASSWORD # to be created in the SES console - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('swiftmailer', [ - 'transport' => 'smtp', - 'host' => 'email-smtp.us-east-1.amazonaws.com', - 'port' => 587, - 'encryption' => 'tls', - 'username' => 'AWS_SES_SMTP_USERNAME', - 'password' => 'AWS_SES_SMTP_PASSWORD', - ]); - -The ``port`` and ``encryption`` keys are not present in the Symfony Standard -Edition configuration by default, but you can add them if needed. - -And that's it, you're ready to start sending emails through the cloud! - -.. tip:: - - 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. - - .. code-block:: yaml - - # app/config/parameters.yml - parameters: - # ... - mailer_transport: smtp - mailer_host: email-smtp.us-east-1.amazonaws.com - mailer_port: 587 # different ports are available, see SES console - mailer_encryption: tls # TLS encryption is required - mailer_user: AWS_SES_SMTP_USERNAME # to be created in the SES console - mailer_password: AWS_SES_SMTP_PASSWORD # to be created in the SES console - -.. note:: - - 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. - -.. _`Amazon's Simple Email Service (SES)`: http://aws.amazon.com/ses -.. _`SES console`: https://console.aws.amazon.com/ses -.. _`Amazon Web Services (AWS)`: http://aws.amazon.com diff --git a/email/dev_environment.rst b/email/dev_environment.rst deleted file mode 100644 index 8224eab88c8..00000000000 --- a/email/dev_environment.rst +++ /dev/null @@ -1,257 +0,0 @@ -.. index:: - single: Emails; In development - -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 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 (with optional exceptions). - -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: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_test.yml - swiftmailer: - disable_delivery: true - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/config_test.php - $container->loadFromExtension('swiftmailer', [ - 'disable_delivery' => "true", - ]); - -If you'd also like to disable deliver in the ``dev`` environment, -add this same configuration to the ``config_dev.yml`` file. - -.. _sending-to-a-specified-address: - -Sending to a Specified Address(es) ----------------------------------- - -You can also choose to have all email sent to a specific address or a list of addresses, instead -of the address actually specified when sending the message. This can be done -via the ``delivery_addresses`` option: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_dev.yml - swiftmailer: - delivery_addresses: ['dev@example.com'] - - .. code-block:: xml - - - - - - - dev@example.com - - - - .. code-block:: php - - // app/config/config_dev.php - $container->loadFromExtension('swiftmailer', [ - 'delivery_addresses' => ["dev@example.com"], - ]); - -Now, suppose you're sending an email to ``recipient@example.com``:: - - public function indexAction($name, \Swift_Mailer $mailer) - { - $message = (new \Swift_Message('Hello Email')) - ->setFrom('send@example.com') - ->setTo('recipient@example.com') - ->setBody( - $this->renderView( - 'HelloBundle:Hello:email.txt.twig', - ['name' => $name] - ) - ) - ; - $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. - -.. note:: - - 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. - -.. _sending-to-a-specified-address-but-with-exceptions: - -Sending to a Specified Address but with Exceptions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Suppose you want to have all email redirected to a specific address, -(like in the above scenario to ``dev@example.com``). But then you may want -email sent to some specific email addresses to go through after all, and -not be redirected (even if it is in the dev environment). This can be done -by adding the ``delivery_whitelist`` option: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_dev.yml - swiftmailer: - delivery_addresses: ['dev@example.com'] - delivery_whitelist: - # all email addresses matching these regexes will be delivered - # like normal, as well as being sent to dev@example.com - - '/@specialdomain\.com$/' - - '/^admin@mydomain\.com$/' - - .. code-block:: xml - - - - - - - - /@specialdomain\.com$/ - /^admin@mydomain\.com$/ - dev@example.com - - - - .. code-block:: php - - // app/config/config_dev.php - $container->loadFromExtension('swiftmailer', [ - 'delivery_addresses' => ["dev@example.com"], - 'delivery_whitelist' => [ - // all email addresses matching these regexes will be delivered - // like normal, as well as being sent to dev@example.com - '/@specialdomain\.com$/', - '/^admin@mydomain\.com$/', - ], - ]); - -In the above example all email messages will be redirected to ``dev@example.com`` -and messages sent to the ``admin@mydomain.com`` address or to any email address -belonging to the domain ``specialdomain.com`` will also be delivered as normal. - -.. caution:: - - The ``delivery_whitelist`` option is ignored unless the ``delivery_addresses`` option is defined. - -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. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_dev.yml - web_profiler: - intercept_redirects: true - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/config_dev.php - $container->loadFromExtension('web_profiler', [ - 'intercept_redirects' => 'true', - ]); - -.. tip:: - - 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. - -.. tip:: - - In addition to the features provided by Symfony, there are applications that - can help you test emails during application development, like `MailCatcher`_ - and `MailHog`_. - -.. _`MailCatcher`: https://github.com/sj26/mailcatcher -.. _`MailHog`: https://github.com/mailhog/MailHog diff --git a/email/gmail.rst b/email/gmail.rst deleted file mode 100644 index 291bafd5956..00000000000 --- a/email/gmail.rst +++ /dev/null @@ -1,133 +0,0 @@ -.. index:: - single: Emails; Gmail - -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. You can achieve this with the -SwiftmailerBundle without much effort. - -In the development configuration file, change the ``transport`` setting to -``gmail`` and set the ``username`` and ``password`` to the Google credentials: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_dev.yml - swiftmailer: - transport: gmail - username: your_gmail_username - password: your_gmail_password - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/config_dev.php - $container->loadFromExtension('swiftmailer', [ - 'transport' => 'gmail', - 'username' => 'your_gmail_username', - 'password' => 'your_gmail_password', - ]); - -.. tip:: - - It's more convenient to configure these options in the ``parameters.yml`` - file: - - .. code-block:: yaml - - # app/config/parameters.yml - parameters: - # ... - mailer_user: your_gmail_username - mailer_password: your_gmail_password - - .. configuration-block:: - - .. code-block:: yaml - - # app/config/config_dev.yml - swiftmailer: - transport: gmail - username: '%mailer_user%' - password: '%mailer_password%' - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/config_dev.php - $container->loadFromExtension('swiftmailer', [ - 'transport' => 'gmail', - 'username' => '%mailer_user%', - 'password' => '%mailer_password%', - ]); - -Redefining the Default Configuration Parameters ------------------------------------------------ - -The ``gmail`` transport is a shortcut that uses the ``smtp`` transport -and sets these options: - -============== ================== -Option Value -============== ================== -``encryption`` ``ssl`` -``auth_mode`` ``login`` -``host`` ``smtp.gmail.com`` -============== ================== - -If your application uses ``tls`` encryption or ``oauth`` authentication, you -must override the default options by defining the ``encryption`` and ``auth_mode`` -parameters. - -If your Gmail account uses 2-Step-Verification, you must `generate an App password`_ -and use it as the value of the ``mailer_password`` parameter. You must also ensure -that you `allow less secure applications to access your Gmail account`_. - -.. seealso:: - - See the :doc:`Swiftmailer configuration reference ` - for more details. - -.. _`generate an App password`: https://support.google.com/accounts/answer/185833 -.. _`allow less secure applications to access your Gmail account`: https://support.google.com/accounts/answer/6010255 diff --git a/email/spool.rst b/email/spool.rst deleted file mode 100644 index ca7b89a76fd..00000000000 --- a/email/spool.rst +++ /dev/null @@ -1,162 +0,0 @@ -.. index:: - single: Emails; Spooling - -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 -Swift Mailer with the memory option, use the following configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - swiftmailer: - # ... - spool: { type: memory } - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('swiftmailer', [ - // ... - 'spool' => ['type' => 'memory'], - ]); - -.. _spool-using-a-file: - -Spool Using Files ------------------- - -When you use the filesystem for spooling, Symfony creates a folder in the given -path for each mail service (e.g. "default" for the default service). This folder -will contain files for each email in the spool. So make sure this directory is -writable by Symfony (or your webserver/PHP)! - -In order to use the spool with files, use the following configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - swiftmailer: - # ... - spool: - type: file - path: /path/to/spooldir - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('swiftmailer', [ - // ... - - 'spool' => [ - 'type' => 'file', - 'path' => '/path/to/spooldir', - ], - ]); - -.. tip:: - - If you want to store the spool somewhere with your project directory, - remember that you can use the ``%kernel.project_dir%`` parameter to reference - the project's root: - - .. code-block:: yaml - - path: '%kernel.project_dir%/app/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: - -.. code-block:: terminal - - $ php bin/console swiftmailer:spool:send --env=prod - -It has an option to limit the number of messages to be sent: - -.. code-block:: terminal - - $ php bin/console swiftmailer:spool:send --message-limit=10 --env=prod - -You can also set the time limit in seconds: - -.. code-block:: terminal - - $ php bin/console swiftmailer:spool:send --time-limit=10 --env=prod - -In practice you will not want to run this manually. Instead, the console command -should be triggered by a cron job or scheduled task and run at a regular -interval. - -.. caution:: - - When you create a message with Swift Mailer, it generates a ``Swift_Message`` - class. If the ``swiftmailer`` service is lazy loaded, it generates instead a - proxy class named ``Swift_Message_``. - - If you use the memory spool, this change is transparent and has no impact. - But when using the filesystem spool, the message class is serialized in - a file with the randomized class name. The problem is that this random - class name changes on every cache clear. - - So if you send a mail and then you clear the cache, on the next execution of - ``swiftmailer:spool:send`` an error will raise because the class - ``Swift_Message_`` doesn't exist (anymore). - - The solutions are either to use the memory spool or to load the - ``swiftmailer`` service without the ``lazy`` option (see :doc:`/service_container/lazy_services`). diff --git a/email/testing.rst b/email/testing.rst deleted file mode 100644 index 58ba1cebd13..00000000000 --- a/email/testing.rst +++ /dev/null @@ -1,85 +0,0 @@ -.. index:: - single: Emails; Testing - -How to Test that an Email is Sent in a Functional Test -====================================================== - -Sending emails 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 :doc:`the Symfony Profiler `. - -Start with a controller action that sends an email:: - - public function sendEmailAction($name, \Swift_Mailer $mailer) - { - $message = (new \Swift_Message('Hello Email')) - ->setFrom('send@example.com') - ->setTo('recipient@example.com') - ->setBody('You should see me from the profiler!') - ; - - $mailer->send($message); - - return $this->render(...); - } - -In your functional test, use the ``swiftmailer`` collector on the profiler -to get information about the messages sent on the previous request:: - - // tests/AppBundle/Controller/MailControllerTest.php - namespace Tests\AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - - class MailControllerTest extends WebTestCase - { - public function testMailIsSentAndContentIsOk() - { - $client = static::createClient(); - - // enables 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'); - - // checks that an email was sent - $this->assertSame(1, $mailCollector->getMessageCount()); - - $collectedMessages = $mailCollector->getMessages(); - $message = $collectedMessages[0]; - - // Asserting email data - $this->assertInstanceOf('Swift_Message', $message); - $this->assertSame('Hello Email', $message->getSubject()); - $this->assertSame('send@example.com', key($message->getFrom())); - $this->assertSame('recipient@example.com', key($message->getTo())); - $this->assertSame( - 'You should see me from the profiler!', - $message->getBody() - ); - } - } - -Troubleshooting ---------------- - -Problem: The Collector Object Is ``null`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The email collector is only available when the profiler is enabled and collects -information, as explained in :doc:`/testing/profiling`. - -Problem: The Collector Doesn't Contain the E-Mail -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If a redirection is performed after sending the email (for example when you send -an email after a form is processed and before redirecting to another page), make -sure that the test client doesn't follow the redirects, as explained in -:doc:`/testing`. Otherwise, the collector will contain the information of the -redirected page and the email won't be accessible. - -.. _`Swift Mailer`: http://swiftmailer.org/ diff --git a/event_dispatcher.rst b/event_dispatcher.rst index ebbb2bcecac..5a20bb05dea 100644 --- a/event_dispatcher.rst +++ b/event_dispatcher.rst @@ -23,19 +23,19 @@ Creating an Event Listener The most common way to listen to an event is to register an **event listener**:: - // src/AppBundle/EventListener/ExceptionListener.php - namespace AppBundle\EventListener; + // src/EventListener/ExceptionListener.php + namespace App\EventListener; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; + use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; class ExceptionListener { - public function onKernelException(GetResponseForExceptionEvent $event) + public function onKernelException(ExceptionEvent $event) { // You get the exception object from the received event - $exception = $event->getException(); + $exception = $event->getThrowable(); $message = sprintf( 'My Error says: %s with code: %s', $exception->getMessage(), @@ -63,11 +63,11 @@ The most common way to listen to an event is to register an **event listener**:: .. tip:: Each event receives a slightly different type of ``$event`` object. For - the ``kernel.exception`` event, it is :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent`. + the ``kernel.exception`` event, it is :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent`. Check out the :doc:`Symfony events reference ` to see what type of object each event provides. -Now that the class is created, you just need to register it as a service and +Now that the class is created, you 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": @@ -75,15 +75,15 @@ using a special "tag": .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: - AppBundle\EventListener\ExceptionListener: + App\EventListener\ExceptionListener: tags: - { name: kernel.event_listener, event: kernel.exception } .. code-block:: xml - + - + @@ -99,27 +99,35 @@ using a special "tag": .. code-block:: php - // app/config/services.php - use AppBundle\EventListener\ExceptionListener; + // config/services.php + use App\EventListener\ExceptionListener; $container ->autowire(ExceptionListener::class) ->addTag('kernel.event_listener', ['event' => 'kernel.exception']) ; -.. note:: +Symfony follows this logic to decide which method to execute inside the event +listener class: + +#. If the ``kernel.event_listener`` tag defines the ``method`` attribute, that's + the name of the method to be executed; +#. If no ``method`` attribute is defined, try to execute the method whose name + is ``on`` + "camel-cased event name" (e.g. ``onKernelException()`` method for + the ``kernel.exception`` event); +#. If that method is not defined either, try to execute the ``__invoke()`` magic + method (which makes event listeners invokable); +#. If the ``_invoke()`` method is not defined either, throw an exception. - There is an optional tag attribute called ``method`` which defines which method - to execute when the event is triggered. By default, the name of the method is - ``on`` + "camel-cased event name". If the event is ``kernel.exception`` the - method executed by default is ``onKernelException()``. +.. note:: - The other optional tag attribute is called ``priority``, which defaults to - ``0`` and it controls the order in which listeners are executed for a given - event (the higher the number the earlier a listener is executed). This is - useful when you need to guarantee that one listener is executed before - another. The priorities of the internal Symfony listeners usually range from - ``-255`` to ``255`` but your own listeners can use any positive or negative integer. + There is an optional attribute for the ``kernel.event_listener`` tag called + ``priority``, which is a positive or negative integer that defaults to ``0`` + and it controls the order in which listeners are executed (the higher the + number, the earlier a listener is executed). This is useful when you need to + guarantee that one listener is executed before another. The priorities of the + internal Symfony listeners usually range from ``-255`` to ``255`` but your + own listeners can use any positive or negative integer. .. _events-subscriber: @@ -141,11 +149,11 @@ and subscribers. To learn more about event subscribers, read :doc:`/components/e The following example shows an event subscriber that defines several methods which listen to the same ``kernel.exception`` event:: - // src/AppBundle/EventSubscriber/ExceptionSubscriber.php - namespace AppBundle\EventSubscriber; + // src/EventSubscriber/ExceptionSubscriber.php + namespace App\EventSubscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; + use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\KernelEvents; class ExceptionSubscriber implements EventSubscriberInterface @@ -162,23 +170,23 @@ listen to the same ``kernel.exception`` event:: ]; } - public function processException(GetResponseForExceptionEvent $event) + public function processException(ExceptionEvent $event) { // ... } - public function logException(GetResponseForExceptionEvent $event) + public function logException(ExceptionEvent $event) { // ... } - public function notifyException(GetResponseForExceptionEvent $event) + public function notifyException(ExceptionEvent $event) { // ... } } -That's it! Your ``services.yml`` file should already be setup to load services from +That's it! Your ``services.yaml`` file should already be setup to load services from the ``EventSubscriber`` directory. Symfony takes care of the rest. .. _ref-event-subscriber-configuration: @@ -194,18 +202,18 @@ Request Events, Checking Types ------------------------------ A single page can make several requests (one master request, and then multiple -sub-requests - typically by :doc:`/templating/embedding_controllers`). For the core -Symfony events, you might need to check to see if the event is for a "master" request -or a "sub request":: +sub-requests - typically when :ref:`embedding controllers in templates `). +For the core Symfony events, you might need to check to see if the event is for +a "master" request or a "sub request":: - // src/AppBundle/EventListener/RequestListener.php - namespace AppBundle\EventListener; + // src/EventListener/RequestListener.php + namespace App\EventListener; - use Symfony\Component\HttpKernel\Event\GetResponseEvent; + use Symfony\Component\HttpKernel\Event\RequestEvent; class RequestListener { - public function onKernelRequest(GetResponseEvent $event) + public function onKernelRequest(RequestEvent $event) { if (!$event->isMasterRequest()) { // don't do anything if it's not the master request @@ -256,6 +264,6 @@ Learn more .. toctree:: :maxdepth: 1 - :glob: - event_dispatcher/* + event_dispatcher/before_after_filters + event_dispatcher/method_behavior diff --git a/event_dispatcher/before_after_filters.rst b/event_dispatcher/before_after_filters.rst index 4b5a36bd401..771163f4cb8 100644 --- a/event_dispatcher/before_after_filters.rst +++ b/event_dispatcher/before_after_filters.rst @@ -33,14 +33,13 @@ token. Before Filters with the ``kernel.controller`` Event --------------------------------------------------- -First, store some basic token configuration using ``config.yml`` and the -parameters key: +First, define some token configuration as parameters: .. configuration-block:: .. code-block:: yaml - # app/config/config.yml + # config/services.yaml parameters: tokens: client1: pass1 @@ -48,7 +47,7 @@ parameters key: .. code-block:: xml - + setParameter('tokens', [ 'client1' => 'pass1', 'client2' => 'pass2', @@ -81,24 +80,24 @@ some way to identify if the controller that matches the request needs token vali A clean and easy way is to create an empty interface and make the controllers implement it:: - namespace AppBundle\Controller; + namespace App\Controller; interface TokenAuthenticatedController { // ... } -A controller that implements this interface simply looks like this:: +A controller that implements this interface looks like this:: - namespace AppBundle\Controller; + namespace App\Controller; - use AppBundle\Controller\TokenAuthenticatedController; - use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use App\Controller\TokenAuthenticatedController; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - class FooController extends Controller implements TokenAuthenticatedController + class FooController extends AbstractController implements TokenAuthenticatedController { // An action that needs authentication - public function barAction() + public function bar() { // ... } @@ -111,12 +110,12 @@ Next, you'll need to create an event subscriber, which will hold the logic that you want to be executed before your controllers. If you're not familiar with event subscribers, you can learn more about them at :doc:`/event_dispatcher`:: - // src/AppBundle/EventSubscriber/TokenSubscriber.php - namespace AppBundle\EventSubscriber; + // src/EventSubscriber/TokenSubscriber.php + namespace App\EventSubscriber; - use AppBundle\Controller\TokenAuthenticatedController; + use App\Controller\TokenAuthenticatedController; use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\FilterControllerEvent; + use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\KernelEvents; @@ -129,7 +128,7 @@ event subscribers, you can learn more about them at :doc:`/event_dispatcher`:: $this->tokens = $tokens; } - public function onKernelController(FilterControllerEvent $event) + public function onKernelController(ControllerEvent $event) { $controller = $event->getController(); @@ -155,7 +154,7 @@ event subscribers, you can learn more about them at :doc:`/event_dispatcher`:: } } -That's it! Your ``services.yml`` file should already be setup to load services from +That's it! Your ``services.yaml`` file should already be setup to load services from the ``EventSubscriber`` directory. Symfony takes care of the rest. Your ``TokenSubscriber`` ``onKernelController()`` method will be executed on each request. If the controller that is about to be executed implements ``TokenAuthenticatedController``, @@ -179,14 +178,14 @@ all responses that have passed this token authentication. Another core Symfony event - called ``kernel.response`` (aka ``KernelEvents::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 +To create an "after" listener, create a listener class and register it as a service on this event. For example, take the ``TokenSubscriber`` 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) + public function onKernelController(ControllerEvent $event) { // ... @@ -206,9 +205,9 @@ This will look for the ``auth_token`` flag on the request object and set a custo 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; + use Symfony\Component\HttpKernel\Event\ResponseEvent; - public function onKernelResponse(FilterResponseEvent $event) + public function onKernelResponse(ResponseEvent $event) { // check to see if onKernelController marked this as a token "auth'ed" request if (!$token = $event->getRequest()->attributes->get('auth_token')) { diff --git a/event_dispatcher/method_behavior.rst b/event_dispatcher/method_behavior.rst index 5ea5e74ffab..7d93d074353 100644 --- a/event_dispatcher/method_behavior.rst +++ b/event_dispatcher/method_behavior.rst @@ -19,7 +19,7 @@ method:: { // dispatch an event before the method $event = new BeforeSendMailEvent($subject, $message); - $this->dispatcher->dispatch('mailer.pre_send', $event); + $this->dispatcher->dispatch($event, 'mailer.pre_send'); // get $foo and $bar from the event, they may have been modified $subject = $event->getSubject(); @@ -30,7 +30,7 @@ method:: // do something after the method $event = new AfterSendMailEvent($returnValue); - $this->dispatcher->dispatch('mailer.post_send', $event); + $this->dispatcher->dispatch($event, 'mailer.post_send'); return $event->getReturnValue(); } @@ -41,10 +41,10 @@ executed, and ``mailer.post_send`` after the method is executed. Each uses a custom Event class to communicate information to the listeners of the two events. For example, ``BeforeSendMailEvent`` might look like this:: - // src/AppBundle/Event/BeforeSendMailEvent.php - namespace AppBundle\Event; + // src/Event/BeforeSendMailEvent.php + namespace App\Event; - use Symfony\Component\EventDispatcher\Event; + use Symfony\Contracts\EventDispatcher\Event; class BeforeSendMailEvent extends Event { @@ -80,10 +80,10 @@ events. For example, ``BeforeSendMailEvent`` might look like this:: And the ``AfterSendMailEvent`` even like this:: - // src/AppBundle/Event/AfterSendMailEvent.php - namespace AppBundle\Event; + // src/Event/AfterSendMailEvent.php + namespace App\Event; - use Symfony\Component\EventDispatcher\Event; + use Symfony\Contracts\EventDispatcher\Event; class AfterSendMailEvent extends Event { @@ -111,10 +111,10 @@ that information (e.g. ``setMessage()``). Now, you can create an event subscriber to hook into this event. For example, you could listen to the ``mailer.post_send`` event and change the method's return value:: - // src/AppBundle/EventSubscriber/MailPostSendSubscriber.php - namespace AppBundle\EventSubscriber; + // src/EventSubscriber/MailPostSendSubscriber.php + namespace App\EventSubscriber; - use AppBundle\Event\AfterSendMailEvent; + use App\Event\AfterSendMailEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class MailPostSendSubscriber implements EventSubscriberInterface diff --git a/form/action_method.rst b/form/action_method.rst deleted file mode 100644 index 06bd7e3a858..00000000000 --- a/form/action_method.rst +++ /dev/null @@ -1,132 +0,0 @@ -.. index:: - single: Forms; Changing the action and method - -How to Change the Action and Method of a Form -============================================= - -By default, a form will be submitted via an HTTP POST request to the same -URL under which the form was rendered. Sometimes you want to change these -parameters. You can do so in a few different ways. - -If you use the :class:`Symfony\\Component\\Form\\FormBuilder` to build your -form, you can use ``setAction()`` and ``setMethod()``: - -.. configuration-block:: - - .. code-block:: php-symfony - - // AppBundle/Controller/DefaultController.php - namespace AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Form\Extension\Core\Type\DateType; - use Symfony\Component\Form\Extension\Core\Type\SubmitType; - use Symfony\Component\Form\Extension\Core\Type\TextType; - - class DefaultController extends Controller - { - public function newAction() - { - // ... - - $form = $this->createFormBuilder($task) - ->setAction($this->generateUrl('target_route')) - ->setMethod('GET') - ->add('task', TextType::class) - ->add('dueDate', DateType::class) - ->add('save', SubmitType::class) - ->getForm(); - - // ... - } - } - - .. code-block:: php-standalone - - use Symfony\Component\Form\Forms; - use Symfony\Component\Form\Extension\Core\Type\DateType; - use Symfony\Component\Form\Extension\Core\Type\FormType; - use Symfony\Component\Form\Extension\Core\Type\SubmitType; - use Symfony\Component\Form\Extension\Core\Type\TextType; - - // ... - - $formFactoryBuilder = Forms::createFormFactoryBuilder(); - - // Form factory builder configuration ... - - $formFactory = $formFactoryBuilder->getFormFactory(); - - $form = $formFactory->createBuilder(FormType::class, $task) - ->setAction('...') - ->setMethod('GET') - ->add('task', TextType::class) - ->add('dueDate', DateType::class) - ->add('save', SubmitType::class) - ->getForm(); - -.. note:: - - This example assumes that you've created a route called ``target_route`` - that points to the controller that processes the form. - -When using a form type class, you can pass the action and method as form -options: - -.. configuration-block:: - - .. code-block:: php-symfony - - // AppBundle/Controller/DefaultController.php - namespace AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use AppBundle\Form\TaskType; - - class DefaultController extends Controller - { - public function newAction() - { - // ... - - $form = $this->createForm(TaskType::class, $task, [ - 'action' => $this->generateUrl('target_route'), - 'method' => 'GET', - ]); - - // ... - } - } - - .. code-block:: php-standalone - - use Symfony\Component\Form\Forms; - use AppBundle\Form\TaskType; - - $formFactoryBuilder = Forms::createFormFactoryBuilder(); - - // Form factory builder configuration ... - - $formFactory = $formFactoryBuilder->getFormFactory(); - - $form = $formFactory->create(TaskType::class, $task, [ - 'action' => '...', - 'method' => 'GET', - ]); - -Finally, you can override the action and method in the template by passing them -to the ``form()`` or the ``form_start()`` helper functions: - -.. code-block:: twig - - {# app/Resources/views/default/new.html.twig #} - {{ form_start(form, {'action': path('target_route'), 'method': 'GET'}) }} - -.. note:: - - 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. See the :ref:`configuration-framework-http_method_override` - option. diff --git a/form/bootstrap4.rst b/form/bootstrap4.rst index f069ae33788..71dbff9d0d6 100644 --- a/form/bootstrap4.rst +++ b/form/bootstrap4.rst @@ -2,7 +2,7 @@ Bootstrap 4 Form Theme ====================== Symfony provides several ways of integrating Bootstrap into your application. The -most straightforward way is to just add the required ```` and `` - {% endjavascripts %} - -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: - -.. code-block:: html+twig - - {% javascripts '@AppBundle/Resources/public/js/example.coffee' - '@AppBundle/Resources/public/js/another.coffee' - filter='coffee' %} - - {% endjavascripts %} - -Both files will now be served up as a single file compiled into regular JavaScript. - -.. _assetic-apply-to: - -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, 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: - -.. configuration-block:: - - .. code-block:: 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$' - - .. code-block:: xml - - - - - - - - /usr/lib/node_modules/ - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', [ - 'filters' => [ - 'coffee' => [ - 'bin' => '/usr/bin/coffee', - 'node' => '/usr/bin/node', - 'node_paths' => ['/usr/lib/node_modules/'], - 'apply_to' => '\.coffee$', - ], - ], - ]); - -With this option, 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): - -.. code-block:: html+twig - - {% javascripts '@AppBundle/Resources/public/js/example.coffee' - '@AppBundle/Resources/public/js/another.coffee' - '@AppBundle/Resources/public/js/regular.js' %} - - {% endjavascripts %} - - diff --git a/frontend/assetic/asset_management.rst b/frontend/assetic/asset_management.rst deleted file mode 100644 index 1f0df69ea2a..00000000000 --- a/frontend/assetic/asset_management.rst +++ /dev/null @@ -1,578 +0,0 @@ -.. index:: - single: Assetic; Introduction - -How to Use Assetic for Asset Management -======================================= - -Installing and Enabling Assetic -------------------------------- - -Starting from Symfony 2.8, Assetic is no longer included by default in the -Symfony Standard Edition. Before using any of its features, install the -AsseticBundle executing this console command in your project: - -.. code-block:: terminal - - $ composer require symfony/assetic-bundle - -Then, enable the bundle in the ``AppKernel.php`` file of your Symfony application:: - - // app/AppKernel.php - - // ... - class AppKernel extends Kernel - { - // ... - - public function registerBundles() - { - $bundles = [ - // ... - new Symfony\Bundle\AsseticBundle\AsseticBundle(), - ]; - - // ... - } - } - -Finally, add the following minimal configuration to enable Assetic support in -your application: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - debug: '%kernel.debug%' - use_controller: '%kernel.debug%' - filters: - cssrewrite: ~ - - # ... - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', [ - 'debug' => '%kernel.debug%', - 'use_controller' => '%kernel.debug%', - 'filters' => [ - 'cssrewrite' => null, - ], - // ... - ]); - - // ... - -Introducing Assetic -------------------- - -Assetic combines two major ideas: :ref:`assets ` and -:ref:`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: - -.. code-block:: html+twig - - - -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 - -.. _assetic-assets: - -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 :ref:`CSS stylesheets `, -:ref:`JavaScript files ` and -:ref:`images `. The philosophy -behind adding either is basically the same, but with a slightly different syntax. - -.. _assetic-including-javascript: - -Including JavaScript Files -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To include JavaScript files, use the ``javascripts`` tag in any template: - -.. code-block:: html+twig - - {% javascripts '@AppBundle/Resources/public/js/*' %} - - {% endjavascripts %} - -.. note:: - - If your application templates use the default block names from the Symfony - Standard Edition, the ``javascripts`` tag will most commonly live in the - ``javascripts`` block: - - .. code-block:: html+twig - - {# ... #} - {% block javascripts %} - {% javascripts '@AppBundle/Resources/public/js/*' %} - - {% endjavascripts %} - {% endblock %} - {# ... #} - -.. tip:: - - You can also include CSS stylesheets: see :ref:`assetic-including-css`. - -In this example, all files in the ``Resources/public/js/`` directory of the -AppBundle will be loaded and served from a different location. The actual -rendered tag might look like: - -.. code-block:: html - - - -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 :ref:`assetic-cssrewrite`. - -.. _assetic-including-css: - -Including CSS Stylesheets -~~~~~~~~~~~~~~~~~~~~~~~~~ - -To bring in CSS stylesheets, you can use the same technique explained above, -except with the ``stylesheets`` tag: - -.. code-block:: html+twig - - {% stylesheets 'bundles/app/css/*' filter='cssrewrite' %} - - {% endstylesheets %} - -.. note:: - - If your application templates use the default block names from the Symfony - Standard Edition, the ``stylesheets`` tag will most commonly live in the - ``stylesheets`` block: - - .. code-block:: html+twig - - {# ... #} - {% block stylesheets %} - {% stylesheets 'bundles/app/css/*' filter='cssrewrite' %} - - {% 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 :ref:`cssrewrite ` filter. - -.. note:: - - 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. - -.. _assetic-including-image: - -Including Images -~~~~~~~~~~~~~~~~ - -To include an image you can use the ``image`` tag. - -.. code-block:: html+twig - - {% image '@AppBundle/Resources/public/images/example.jpg' %} - Example - {% endimage %} - -You can also use Assetic for image optimization. More information in -:doc:`/frontend/assetic/jpeg_optimize`. - -.. tip:: - - Instead of using Assetic to include images, you may consider using the - `LiipImagineBundle`_ community bundle, which allows to compress and - manipulate images (rotate, resize, watermark, etc.) before serving them. - -.. _assetic-cssrewrite: - -Fixing CSS Paths with the ``cssrewrite`` Filter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Since Assetic generates new URLs for your assets, any relative paths inside -your CSS files will break. To fix this, make sure to use the ``cssrewrite`` -filter with your ``stylesheets`` tag. This parses your CSS files and corrects -the paths internally to reflect the new location. - -You can see an example in the previous section. - -.. caution:: - - When using the ``cssrewrite`` filter, don't refer to your CSS files using - the ``@AppBundle`` syntax. See the note in the above section for details. - -Combining Assets -~~~~~~~~~~~~~~~~ - -One feature of Assetic is that it will combine many files into one. This helps -to reduce the number of HTTP requests, which is great for front-end performance. -It also allows you to maintain the files by splitting them into smaller, more -manageable parts. This can help with re-usability as you can split project-specific -files from those which can be used in other applications, but still serve them as a -single file: - -.. code-block:: html+twig - - {% javascripts - '@AppBundle/Resources/public/js/*' - '@AcmeBarBundle/Resources/public/js/form.js' - '@AcmeBarBundle/Resources/public/js/calendar.js' %} - - {% endjavascripts %} - -In the ``dev`` environment, each file is still served individually, so that you -can debug problems. 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. - -.. tip:: - - 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 :ref:`assetic-dumping`. - -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: - -.. code-block:: html+twig - - {% javascripts - '@AppBundle/Resources/public/js/thirdparty/jquery.js' - '@AppBundle/Resources/public/js/*' %} - - {% endjavascripts %} - -Using Named Assets -~~~~~~~~~~~~~~~~~~ - -AsseticBundle configuration directives allow you to define named asset sets. -You can do so by defining the input files, filters and output files in your -configuration under the ``assetic`` section. Read more in the -:doc:`assetic config reference `. - -.. configuration-block:: - - .. code-block:: 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' - - .. code-block:: xml - - - - - - - - @AppBundle/Resources/public/js/thirdparty/jquery.js - @AppBundle/Resources/public/js/thirdparty/jquery.ui.js - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', [ - 'assets' => [ - 'jquery_and_ui' => [ - 'inputs' => [ - '@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: - -.. code-block:: html+twig - - {% javascripts - '@jquery_and_ui' - '@AppBundle/Resources/public/js/*' %} - - {% endjavascripts %} - -.. _assetic-filters: - -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 frontend optimization). Other filters -can compile CoffeeScript files to JavaScript 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 configuration -should be defined: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - uglifyjs2: - bin: /usr/local/bin/uglifyjs - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', [ - 'filters' => [ - 'uglifyjs2' => [ - 'bin' => '/usr/local/bin/uglifyjs', - ], - ], - ]); - -Now, to actually *use* the filter on a group of JavaScript files, add it -into your template: - -.. code-block:: html+twig - - {% javascripts '@AppBundle/Resources/public/js/*' filter='uglifyjs2' %} - - {% endjavascripts %} - -A more detailed guide about configuring and using Assetic filters as well as -details of Assetic's debug mode can be found in :doc:`/frontend/assetic/uglifyjs`. - -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: - -.. code-block:: html+twig - - {% javascripts '@AppBundle/Resources/public/js/*' output='js/compiled/main.js' %} - - {% endjavascripts %} - -.. note:: - - Symfony provides various cache busting implementations via the - :ref:`version `, - :ref:`version_format `, and - :ref:`json_manifest_path ` - configuration options. - -.. _assetic-dumping: - -Dumping Asset Files -------------------- - -In the ``dev`` environment, Assetic generates paths to CSS and JavaScript -files that don't physically exist on your computer. But they render nonetheless -because an internal Symfony controller opens the files and serves back the -content (after running any filters). - -This kind of dynamic serving of processed assets is great because it means -that you can immediately see the new state of any asset files you change. -It's also bad, because it can be quite slow. If you're using a lot of filters, -it might be downright frustrating. - -Fortunately, Assetic provides a way to dump your assets to real files, instead -of being generated dynamically. - -Dumping Asset Files in the ``prod`` Environment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In the ``prod`` environment, your JS and CSS files are represented by a single -tag each. In other words, instead of seeing each JavaScript file you're including -in your source, you'll likely just see something like this: - -.. code-block:: html - - - -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. - -.. _assetic-dump-prod: - -Instead, each time you use your application in the ``prod`` environment (and therefore, -each time you deploy), you should run the following command: - -.. code-block:: terminal - - $ php bin/console assetic:dump --env=prod --no-debug - -This will physically generate and write each file that you need (e.g. ``/js/abcd123.js``). -If you update any of your assets, you'll need to run this again to regenerate -the file. - -Dumping Asset Files in the ``dev`` Environment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -By default, each asset path generated in the ``dev`` environment is handled -dynamically by Symfony. This has no disadvantage (you can see your changes -immediately), except that assets can load noticeably slow. If you feel like -your assets are loading too slowly, follow this guide. - -First, tell Symfony to stop trying to process these files dynamically. Make -the following change in your ``config_dev.yml`` file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_dev.yml - assetic: - use_controller: false - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/config_dev.php - $container->loadFromExtension('assetic', [ - '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 command: - -.. code-block:: terminal - - $ php bin/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 using the ``assetic:watch`` command, -assets will be regenerated automatically *as they change*: - -.. code-block:: terminal - - $ php bin/console assetic:watch - -The ``assetic:watch`` command was introduced in AsseticBundle 2.4. In prior -versions, you had to use the ``--watch`` option of the ``assetic:dump`` -command for the same behavior. - -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: - -.. code-block:: html+twig - - {% javascripts '@AppBundle/Resources/public/js/*' output='js/compiled/main.js' %} - - {% endjavascripts %} - -.. _`LiipImagineBundle`: https://github.com/liip/LiipImagineBundle diff --git a/frontend/assetic/index.rst b/frontend/assetic/index.rst new file mode 100644 index 00000000000..63955c9f8dd --- /dev/null +++ b/frontend/assetic/index.rst @@ -0,0 +1,8 @@ +Assetic +======= + +.. caution:: + + Using Assetic to manage web assets in Symfony applications is no longer + recommended. Instead, use :doc:`Webpack Encore `, which bridges + Symfony applications with modern JavaScript-based tools to manage web assets. diff --git a/frontend/assetic/jpeg_optimize.rst b/frontend/assetic/jpeg_optimize.rst deleted file mode 100644 index b4253a0de56..00000000000 --- a/frontend/assetic/jpeg_optimize.rst +++ /dev/null @@ -1,299 +0,0 @@ -.. index:: - single: Assetic; Image optimization - -How to Use Assetic for Image Optimization with Twig Functions -============================================================= - -.. include:: /assetic/_standard_edition_warning.rst.inc - -Among 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, make -sure to have it already installed on your system and then, configure its location -using the ``bin`` option of the ``jpegoptim`` filter: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - jpegoptim: - bin: path/to/jpegoptim - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', [ - 'filters' => [ - 'jpegoptim' => [ - 'bin' => 'path/to/jpegoptim', - ], - ], - ]); - -It can now be used from a template: - -.. code-block:: html+twig - - {% image '@AppBundle/Resources/public/images/example.jpg' - filter='jpegoptim' output='/images/example.jpg' %} - Example - {% endimage %} - -Removing all EXIF Data -~~~~~~~~~~~~~~~~~~~~~~ - -By default, the ``jpegoptim`` filter removes some meta information stored -in the image. To remove all EXIF data and comments, set the ``strip_all`` option -to ``true``: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - jpegoptim: - bin: path/to/jpegoptim - strip_all: true - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', [ - 'filters' => [ - 'jpegoptim' => [ - 'bin' => 'path/to/jpegoptim', - 'strip_all' => 'true', - ], - ], - ]); - -Lowering Maximum Quality -~~~~~~~~~~~~~~~~~~~~~~~~ - -By default, the ``jpegoptim`` filter doesn't alter the quality level of the JPEG -image. Use the ``max`` option to configure the maximum quality setting (in a -scale of ``0`` to ``100``). The reduction in the image file size will -be at the expense of its quality: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - jpegoptim: - bin: path/to/jpegoptim - max: 70 - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', [ - 'filters' => [ - 'jpegoptim' => [ - '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 configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - jpegoptim: - bin: path/to/jpegoptim - twig: - functions: - jpegoptim: ~ - - .. code-block:: xml - - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', [ - 'filters' => [ - 'jpegoptim' => [ - 'bin' => 'path/to/jpegoptim', - ], - ], - 'twig' => [ - 'functions' => ['jpegoptim'], - ], - ]); - -The Twig template can now be changed to the following: - -.. code-block:: html+twig - - Example - -You can also specify the output directory for images in the Assetic configuration -file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - jpegoptim: - bin: path/to/jpegoptim - twig: - functions: - jpegoptim: { output: images/*.jpg } - - .. code-block:: xml - - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', [ - 'filters' => [ - 'jpegoptim' => [ - 'bin' => 'path/to/jpegoptim', - ], - ], - 'twig' => [ - 'functions' => [ - 'jpegoptim' => [ - 'output' => 'images/*.jpg', - ], - ], - ], - ]); - -.. tip:: - - For uploaded images, you can compress and manipulate them using the - `LiipImagineBundle`_ community bundle. - -.. _`Jpegoptim`: http://www.kokkonen.net/tjko/projects.html -.. _`LiipImagineBundle`: https://github.com/liip/LiipImagineBundle diff --git a/frontend/assetic/php.rst b/frontend/assetic/php.rst deleted file mode 100644 index 3f2db541d2e..00000000000 --- a/frontend/assetic/php.rst +++ /dev/null @@ -1,213 +0,0 @@ -.. index:: - single: Front-end; Assetic, Bootstrap - -Combining, Compiling and Minimizing Web Assets with PHP Libraries -================================================================= - -.. include:: /assetic/_standard_edition_warning.rst.inc - -The official Symfony Best Practices recommend to use Assetic to -:doc:`manage web assets `, unless you are -comfortable with JavaScript-based front-end tools. - -Even if those JavaScript-based solutions are the most suitable ones from a -technical point of view, using pure PHP alternative libraries can be useful in -some scenarios: - -* If you can't install or use ``npm`` and the other JavaScript solutions; -* If you prefer to limit the amount of different technologies used in your - applications; -* If you want to simplify application deployment. - -In this article, you'll learn how to combine and minimize CSS and JavaScript files -and how to compile Sass files using PHP-only libraries with Assetic. - -Installing the Third-Party Compression Libraries ------------------------------------------------- - -Assetic includes a lot of ready-to-use filters, but it doesn't include their -associated libraries. Therefore, before enabling the filters used in this article, -you must install two libraries. Open a command console, browse to your project -directory and execute the following commands: - -.. code-block:: terminal - - $ composer require leafo/scssphp - $ composer require patchwork/jsqueeze - -Organizing your Web Asset Files -------------------------------- - -This example will include a setup using the Bootstrap CSS framework, jQuery, FontAwesome -and some regular CSS and JavaScript application files (called ``main.css`` and -``main.js``). The recommended directory structure for this set-up looks like this: - -.. code-block:: text - - web/assets/ - ├── css - │ ├── main.css - │ └── code-highlight.css - ├── js - │ ├── bootstrap.js - │ ├── jquery.js - │ └── main.js - └── scss - ├── bootstrap - │ ├── _alerts.scss - │ ├── ... - │ ├── _variables.scss - │ ├── _wells.scss - │ └── mixins - │ ├── _alerts.scss - │ ├── ... - │ └── _vendor-prefixes.scss - ├── bootstrap.scss - ├── font-awesome - │ ├── _animated.scss - │ ├── ... - │ └── _variables.scss - └── font-awesome.scss - -Combining and Minimizing CSS Files and Compiling SCSS Files ------------------------------------------------------------ - -First, configure a new ``scssphp`` Assetic filter: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - scssphp: - formatter: 'Leafo\ScssPhp\Formatter\Compressed' - # ... - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', [ - 'filters' => [ - 'scssphp' => [ - 'formatter' => 'Leafo\ScssPhp\Formatter\Compressed', - ], - // ... - ], - ]); - -The value of the ``formatter`` option is the fully qualified class name of the -formatter used by the filter to produce the compiled CSS file. Using the -compressed formatter will minimize the resulting file, regardless of whether -the original files are regular CSS files or SCSS files. - -Next, update your Twig template to add the ``{% stylesheets %}`` tag defined -by Assetic: - -.. code-block:: html+twig - - {# app/Resources/views/base.html.twig #} - - - - - - {% stylesheets filter="scssphp" output="css/app.css" - "assets/scss/bootstrap.scss" - "assets/scss/font-awesome.scss" - "assets/css/*.css" - %} - - {% endstylesheets %} - -This simple configuration compiles, combines and minifies the SCSS files into a -regular CSS file that's put in ``web/css/app.css``. This is the only CSS file -which will be served to your visitors. - -Combining and Minimizing JavaScript Files ------------------------------------------ - -First, configure a new ``jsqueeze`` Assetic filter as follows: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - jsqueeze: ~ - # ... - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', [ - 'filters' => [ - 'jsqueeze' => null, - // ... - ], - ]); - -Next, update the code of your Twig template to add the ``{% javascripts %}`` tag -defined by Assetic: - -.. code-block:: html+twig - - - - {% javascripts filter="?jsqueeze" output="js/app.js" - "assets/js/jquery.js" - "assets/js/bootstrap.js" - "assets/js/main.js" - %} - - {% endjavascripts %} - - - - -This simple configuration combines all the JavaScript files, minimizes the contents -and saves the output in the ``web/js/app.js`` file, which is the one that is -served to your visitors. - -The leading ``?`` character in the ``jsqueeze`` filter name tells Assetic to only -apply the filter when *not* in ``debug`` mode. In practice, this means that you'll -see unminified files while developing and minimized files in the ``prod`` environment. diff --git a/frontend/assetic/uglifyjs.rst b/frontend/assetic/uglifyjs.rst deleted file mode 100644 index af46db3e775..00000000000 --- a/frontend/assetic/uglifyjs.rst +++ /dev/null @@ -1,310 +0,0 @@ -.. index:: - single: Assetic; UglifyJS - -How to Minify CSS/JS Files (Using UglifyJS and UglifyCSS) -========================================================= - -.. include:: /assetic/_standard_edition_warning.rst.inc - -`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 article, 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 a `Node.js`_ module. First, you need to `install Node.js`_ -and then, decide the installation method: global or local. - -.. caution:: - - Some Linux distributions rename the Node.js binary from ``node`` to ``nodejs``. - This may result in errors like *"/usr/bin/env: node: No such file or - directory"*. You can solve this problem with a symlink: - - .. code-block:: terminal - - $ ln -s /usr/bin/nodejs /usr/bin/node - -Global Installation -~~~~~~~~~~~~~~~~~~~ - -The global installation method makes all your projects use the very same UglifyJS -version, which simplifies its maintenance. Open your command console and execute -the following command (you may need to run it as a root user): - -.. code-block:: terminal - - $ npm install -g uglify-js - -Now you can execute the global ``uglifyjs`` command anywhere on your system: - -.. code-block:: terminal - - $ uglifyjs --help - -Local Installation -~~~~~~~~~~~~~~~~~~ - -It's also possible to install UglifyJS inside your project only, which is useful -when your project requires a specific UglifyJS version. To do this, install it -without the ``-g`` option and specify the path where to put the module: - -.. code-block:: terminal - - $ cd /path/to/your/symfony/project - $ 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. - -Now you can execute the ``uglifyjs`` command that lives in the ``node_modules`` -directory: - -.. code-block:: terminal - - $ "./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: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - uglifyjs2: - # the path to the uglifyjs executable - bin: /usr/local/bin/uglifyjs - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', [ - 'filters' => [ - 'uglifyjs2' => [ - // the path to the uglifyjs executable - 'bin' => '/usr/local/bin/uglifyjs', - ], - ], - ]); - -.. note:: - - The path where UglifyJS is installed may vary depending on your system. - To find out where npm stores the ``bin`` folder, execute the following command: - - .. code-block:: terminal - - $ 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: - -.. configuration-block:: - - .. code-block:: 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 - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', [ - 'node' => '/usr/bin/nodejs', - 'uglifyjs2' => [ - // the path to the uglifyjs executable - 'bin' => '/usr/local/bin/uglifyjs', - ], - ]); - -Minify your Assets ------------------- - -In order to apply UglifyJS on your assets, add the ``filter`` option in the -asset tags of your templates to tell Assetic to use the ``uglifyjs2`` filter: - -.. code-block:: html+twig - - {% javascripts '@AppBundle/Resources/public/js/*' filter='uglifyjs2' %} - - {% endjavascripts %} - -.. note:: - - 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. However, you can include your JavaScript files no matter where they are. - -With the addition of the ``uglifyjs2`` filter to the asset tags above, you -should now see minified JavaScripts coming over the wire much faster. - -Disable Minification in Debug Mode -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Minified JavaScripts are very difficult to read, let alone debug. Because of -this, Assetic lets you disable a certain filter when your application is in -debug (e.g. ``app_dev.php``) mode. You can do this by prefixing the filter name -in your template with a question mark: ``?``. This tells Assetic to only -apply this filter when debug mode is off (e.g. ``app.php``): - -.. code-block:: html+twig - - {% javascripts '@AppBundle/Resources/public/js/*' filter='?uglifyjs2' %} - - {% endjavascripts %} - -To try this out, switch to your ``prod`` environment (``app.php``). But before -you do, don't forget to :ref:`clear your cache ` -and :ref:`dump your assetic assets `. - -.. tip:: - - Instead of adding the filters to the asset tags, you can also configure which - filters to apply for each file in your application configuration file. - See :ref:`assetic-apply-to` for more details. - -Install, Configure and Use UglifyCSS ------------------------------------- - -The usage of UglifyCSS works the same way as UglifyJS. First, make sure -the node package is installed: - -.. code-block:: terminal - - # global installation - $ npm install -g uglifycss - - # local installation - $ cd /path/to/your/symfony/project - $ npm install uglifycss --prefix app/Resources - -Next, add the configuration for this filter: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - filters: - uglifycss: - bin: /usr/local/bin/uglifycss - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', [ - 'filters' => [ - 'uglifycss' => [ - 'bin' => '/usr/local/bin/uglifycss', - ], - ], - ]); - -To use the filter for your CSS files, add the filter to the Assetic ``stylesheets`` -helper: - -.. code-block:: html+twig - - {% stylesheets 'bundles/App/css/*' filter='uglifycss' filter='cssrewrite' %} - - {% endstylesheets %} - -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. - -.. _`UglifyJS`: https://github.com/mishoo/UglifyJS -.. _`UglifyCSS`: https://github.com/fmarcia/UglifyCSS -.. _`Node.js`: https://nodejs.org/ -.. _`install Node.js`: https://nodejs.org/ -.. _`package.json`: http://browsenpm.org/package.json diff --git a/frontend/assetic/yuicompressor.rst b/frontend/assetic/yuicompressor.rst deleted file mode 100644 index 7a96df63642..00000000000 --- a/frontend/assetic/yuicompressor.rst +++ /dev/null @@ -1,147 +0,0 @@ -.. index:: - single: Assetic; YUI Compressor - -How to Minify JavaScripts and Stylesheets with YUI Compressor -============================================================= - -.. caution:: - - The YUI Compressor is `no longer maintained by Yahoo`_. That's why you are - **strongly advised to avoid using YUI utilities** unless strictly necessary. - Read :doc:`/frontend/assetic/uglifyjs` for a modern and up-to-date alternative. - -.. include:: /assetic/_standard_edition_warning.rst.inc - -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. - -Download the YUI Compressor JAR -------------------------------- - -The YUI Compressor is written in Java and distributed as a JAR. `Download the JAR`_ -from the Yahoo! website 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: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - assetic: - # java: '/usr/bin/java' - filters: - yui_css: - jar: '%kernel.project_dir%/app/Resources/java/yuicompressor.jar' - yui_js: - jar: '%kernel.project_dir%/app/Resources/java/yuicompressor.jar' - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('assetic', [ - // 'java' => '/usr/bin/java', - 'filters' => [ - 'yui_css' => [ - 'jar' => '%kernel.project_dir%/app/Resources/java/yuicompressor.jar', - ], - 'yui_js' => [ - 'jar' => '%kernel.project_dir%/app/Resources/java/yuicompressor.jar', - ], - ], - ]); - -.. note:: - - 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: - -.. code-block:: html+twig - - {% javascripts '@AppBundle/Resources/public/js/*' filter='yui_js' %} - - {% endjavascripts %} - -.. note:: - - 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. - -.. code-block:: html+twig - - {% stylesheets '@AppBundle/Resources/public/css/*' filter='yui_css' %} - - {% endstylesheets %} - -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. - -.. code-block:: html+twig - - {% javascripts '@AppBundle/Resources/public/js/*' filter='?yui_js' %} - - {% endjavascripts %} - -.. tip:: - - 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 :ref:`assetic-apply-to`. - -.. _`YUI Compressor`: http://yui.github.io/yuicompressor/ -.. _`Download the JAR`: https://github.com/yui/yuicompressor/releases -.. _`no longer maintained by Yahoo`: http://yuiblog.com/blog/2013/01/24/yui-compressor-has-a-new-owner/ diff --git a/frontend/custom_version_strategy.rst b/frontend/custom_version_strategy.rst index ba56f1f7689..6f350d1bd35 100644 --- a/frontend/custom_version_strategy.rst +++ b/frontend/custom_version_strategy.rst @@ -45,8 +45,8 @@ In this example, the constructor of the class takes as arguments the path to the manifest file generated by `gulp-buster`_ and the format of the generated version string:: - // src/AppBundle/Asset/VersionStrategy/GulpBusterVersionStrategy.php - namespace AppBundle\Asset\VersionStrategy; + // src/Asset/VersionStrategy/GulpBusterVersionStrategy.php + namespace App\Asset\VersionStrategy; use Symfony\Component\Asset\VersionStrategy\VersionStrategyInterface; @@ -112,9 +112,9 @@ After creating the strategy PHP class, register it as a Symfony service. .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: - AppBundle\Asset\VersionStrategy\GulpBusterVersionStrategy: + App\Asset\VersionStrategy\GulpBusterVersionStrategy: arguments: - "%kernel.project_dir%/busters.json" - "%%s?version=%%s" @@ -122,7 +122,7 @@ After creating the strategy PHP class, register it as a Symfony service. .. code-block:: xml - + - + %kernel.project_dir%/busters.json %%s?version=%%s @@ -139,8 +139,8 @@ After creating the strategy PHP class, register it as a Symfony service. .. code-block:: php - // app/config/services.php - use AppBundle\Asset\VersionStrategy\GulpBusterVersionStrategy; + // config/services.php + use App\Asset\VersionStrategy\GulpBusterVersionStrategy; use Symfony\Component\DependencyInjection\Definition; $container->autowire(GulpBusterVersionStrategy::class) @@ -159,15 +159,15 @@ the :ref:`version_strategy ` option: .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: # ... assets: - version_strategy: 'AppBundle\Asset\VersionStrategy\GulpBusterVersionStrategy' + version_strategy: 'App\Asset\VersionStrategy\GulpBusterVersionStrategy' .. code-block:: xml - + ` option: http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + .. code-block:: php - // app/config/config.php - use AppBundle\Asset\VersionStrategy\GulpBusterVersionStrategy; + // config/packages/framework.php + use App\Asset\VersionStrategy\GulpBusterVersionStrategy; $container->loadFromExtension('framework', [ // ... diff --git a/frontend/encore/advanced-config.rst b/frontend/encore/advanced-config.rst index 2c309d9b466..ad529d85850 100644 --- a/frontend/encore/advanced-config.rst +++ b/frontend/encore/advanced-config.rst @@ -63,7 +63,7 @@ state of the current configuration to build a new one: // define the first configuration Encore - .setOutputPath('web/build/first_build/') + .setOutputPath('public/build/first_build/') .setPublicPath('/build/first_build') .addEntry('app', './assets/js/app.js') .addStyleEntry('global', './assets/css/global.scss') @@ -83,7 +83,7 @@ state of the current configuration to build a new one: // define the second configuration Encore - .setOutputPath('web/build/second_build/') + .setOutputPath('public/build/second_build/') .setPublicPath('/build/second_build') .addEntry('mobile', './assets/js/mobile.js') .addStyleEntry('mobile', './assets/css/mobile.less') @@ -111,23 +111,23 @@ Next, define the output directories of each build: .. code-block:: yaml - # app/config/config.yml + # config/packages/webpack_encore.yaml webpack_encore: - output_path: '%kernel.project_dir%/web/default_build' + output_path: '%kernel.project_dir%/public/default_build' builds: - firstConfig: '%kernel.project_dir%/web/first_build' - secondConfig: '%kernel.project_dir%/web/second_build' + firstConfig: '%kernel.project_dir%/public/first_build' + secondConfig: '%kernel.project_dir%/public/second_build' Finally, use the third optional parameter of the ``encore_entry_*_tags()`` functions to specify which build to use: .. code-block:: twig - {# Using the entrypoints.json file located in ./web/first_build #} + {# Using the entrypoints.json file located in ./public/first_build #} {{ encore_entry_script_tags('app', null, 'firstConfig') }} {{ encore_entry_link_tags('global', null, 'firstConfig') }} - {# Using the entrypoints.json file located in ./web/second_build #} + {# Using the entrypoints.json file located in ./public/second_build #} {{ encore_entry_script_tags('mobile', null, 'secondConfig') }} {{ encore_entry_link_tags('mobile', null, 'secondConfig') }} diff --git a/frontend/encore/cdn.rst b/frontend/encore/cdn.rst index 0c486f06057..a7a2884c13a 100644 --- a/frontend/encore/cdn.rst +++ b/frontend/encore/cdn.rst @@ -10,7 +10,7 @@ built files are uploaded to the CDN, configure it in Encore: // ... Encore - .setOutputPath('web/build/') + .setOutputPath('public/build/') // in dev mode, don't use the CDN .setPublicPath('/build'); // ... diff --git a/frontend/encore/copy-files.rst b/frontend/encore/copy-files.rst index c8d3edbb243..f1c51fb5a9d 100644 --- a/frontend/encore/copy-files.rst +++ b/frontend/encore/copy-files.rst @@ -36,7 +36,7 @@ files into your final output directory. Encore // ... - .setOutputPath('web/build/') + .setOutputPath('public/build/') + .copyFiles({ + from: './assets/images', @@ -51,7 +51,7 @@ files into your final output directory. + //pattern: /\.(png|jpg|jpeg)$/ + }) -This will copy all files from ``assets/images`` into ``web/build`` (the output +This will copy all files from ``assets/images`` into ``public/build`` (the output path). If you have :doc:`versioning enabled `, the copied files will include a hash based on their content. @@ -59,10 +59,10 @@ To render inside Twig, use the ``asset()`` function: .. code-block:: html+twig - {# assets/images/logo.png was copied to web/build/logo.png #} + {# assets/images/logo.png was copied to public/build/logo.png #} - {# assets/images/subdir/logo.png was copied to web/build/subdir/logo.png #} + {# assets/images/subdir/logo.png was copied to public/build/subdir/logo.png #} Make sure you've enabled the :ref:`json_manifest_path ` option, diff --git a/frontend/encore/faq.rst b/frontend/encore/faq.rst index 48c8a07b88d..3c621c3b8d0 100644 --- a/frontend/encore/faq.rst +++ b/frontend/encore/faq.rst @@ -29,7 +29,7 @@ server. **2) Only Deploy the Built Assets** The *only* files that need to be deployed to your production servers are the -final, built assets (e.g. the ``web/build`` directory). You do *not* need to install +final, built assets (e.g. the ``public/build`` directory). You do *not* need to install Node.js, deploy ``webpack.config.js``, the ``node_modules`` directory or even your source asset files, **unless** you plan on running ``encore production`` on your production machine. Once your assets are built, these are the *only* thing that need to live @@ -51,7 +51,7 @@ and the built files. Your ``.gitignore`` file should include: /node_modules/ # whatever path you're passing to Encore.setOutputPath() - /web/build + /public/build You *should* commit all of your source asset files, ``package.json`` and ``yarn.lock``. @@ -67,7 +67,7 @@ like ``/myAppSubdir``), you will need to configure that when calling ``Encore.se Encore // ... - .setOutputPath('web/build/') + .setOutputPath('public/build/') - .setPublicPath('/build') + // this is your *true* public path @@ -116,7 +116,7 @@ But, instead of working, you see an error: This dependency was not found: - * respond.js in ./app/Resources/assets/js/app.js + * respond.js in ./assets/js/app.js Typically, a package will "advertise" its "main" file by adding a ``main`` key to its ``package.json``. But sometimes, old libraries won't have this. Instead, you'll diff --git a/frontend/encore/installation.rst b/frontend/encore/installation.rst index 1603fc7dd98..fb48dba00d1 100644 --- a/frontend/encore/installation.rst +++ b/frontend/encore/installation.rst @@ -16,7 +16,7 @@ project: $ composer require symfony/webpack-encore-bundle $ yarn install -If you are using :doc:`Symfony Flex `, this will install and enable +If you are using :ref:`Symfony Flex `, this will install and enable the `WebpackEncoreBundle`_, create the ``assets/`` directory, add a ``webpack.config.js`` file, and add ``node_modules/`` to ``.gitignore``. You can skip the rest of this article and go write your first JavaScript and CSS by @@ -58,7 +58,7 @@ is the main config file for both Webpack and Webpack Encore: Encore // directory where compiled assets will be stored - .setOutputPath('web/build/') + .setOutputPath('public/build/') // public path used by the web server to access the output path .setPublicPath('/build') // only needed for CDN's or sub-directory deploy @@ -99,7 +99,7 @@ is the main config file for both Webpack and Webpack Encore: module.exports = Encore.getWebpackConfig(); Next, create a new ``assets/js/app.js`` file with some basic JavaScript *and* -import some JavaScript: +import some CSS: .. code-block:: javascript diff --git a/frontend/encore/reactjs.rst b/frontend/encore/reactjs.rst index 0039cece794..7cc808a9e55 100644 --- a/frontend/encore/reactjs.rst +++ b/frontend/encore/reactjs.rst @@ -3,6 +3,13 @@ Enabling React.js Using React? First enable support for it in ``webpack.config.js``: +.. code-block:: terminal + + $ yarn add @babel/preset-react --dev + $ yarn add react react-dom prop-types + +Enable react in your ``webpack.config.js``: + .. code-block:: diff // webpack.config.js diff --git a/frontend/encore/simple-example.rst b/frontend/encore/simple-example.rst index 5937d19a952..0a90d55ea04 100644 --- a/frontend/encore/simple-example.rst +++ b/frontend/encore/simple-example.rst @@ -39,7 +39,7 @@ of your project. It already holds the basic config you need: Encore // directory where compiled assets will be stored - .setOutputPath('web/build/') + .setOutputPath('public/build/') // public path used by the web server to access the output path .setPublicPath('/build') @@ -53,7 +53,7 @@ of your project. It already holds the basic config you need: They *key* part is ``addEntry()``: this tells Encore to load the ``assets/js/app.js`` file and follow *all* of the ``require()`` statements. It will then package everything together and - thanks to the first ``app`` argument - output final ``app.js`` and -``app.css`` files into the ``web/build`` directory. +``app.css`` files into the ``public/build`` directory. .. _encore-build-assets: @@ -76,16 +76,16 @@ To build the assets, run: Congrats! You now have three new files: -* ``web/build/app.js`` (holds all the JavaScript for your "app" entry) -* ``web/build/app.css`` (holds all the CSS for your "app" entry) -* ``web/build/runtime.js`` (a file that helps Webpack do its job) +* ``public/build/app.js`` (holds all the JavaScript for your "app" entry) +* ``public/build/app.css`` (holds all the CSS for your "app" entry) +* ``public/build/runtime.js`` (a file that helps Webpack do its job) Next, include these in your base layout file. Two Twig helpers from WebpackEncoreBundle can do most of the work for you: .. code-block:: html+twig - {# app/Resources/views/base.html.twig #} + {# templates/base.html.twig #} @@ -138,7 +138,7 @@ some optional features. Requiring JavaScript Modules ---------------------------- -Webpack is a module bundler... which means that you can ``require`` other JavaScript +Webpack is a module bundler, which means that you can ``require`` other JavaScript files. First, create a file that exports a function: .. code-block:: javascript @@ -183,7 +183,7 @@ Instead of using ``require()`` and ``module.exports`` like shown above, JavaScri provides an alternate syntax based on the `ECMAScript 6 modules`_ that includes the ability to use dynamic imports. -To export values, use ``export``: +To export values using the alternate syntax, use ``export``: .. code-block:: diff diff --git a/frontend/encore/versioning.rst b/frontend/encore/versioning.rst index 8776cda0ca8..6f9b245a05e 100644 --- a/frontend/encore/versioning.rst +++ b/frontend/encore/versioning.rst @@ -16,7 +16,7 @@ ignoring any existing cache: // ... Encore - .setOutputPath('web/build/') + .setOutputPath('public/build/') // ... + .enableVersioning() @@ -51,18 +51,17 @@ the CSS and JavaScript files): In your app, you need to read this file if you want to be able to link (e.g. via an ``img`` tag) to certain assets. If you're using Symfony, just activate the -``json_manifest_file`` versioning strategy in ``config.yml``: +``json_manifest_file`` versioning strategy: .. code-block:: yaml - # app/config/config.yml + # this file is added automatically when installing Encore with Symfony Flex + # config/packages/assets.yaml framework: - # ... assets: - # feature is supported in Symfony 3.3 and higher - json_manifest_path: '%kernel.project_dir%/web/build/manifest.json' + json_manifest_path: '%kernel.project_dir%/public/build/manifest.json' -That's it! Just be sure to wrap each path in the Twig ``asset()`` function +That's it! Be sure to wrap each path in the Twig ``asset()`` function like normal: .. code-block:: html+twig diff --git a/frontend/encore/versus-assetic.rst b/frontend/encore/versus-assetic.rst index afc2373b2b7..650ade4e0f3 100644 --- a/frontend/encore/versus-assetic.rst +++ b/frontend/encore/versus-assetic.rst @@ -1,7 +1,7 @@ Encore Versus Assetic? ====================== -Symfony originally shipped with support for :doc:`Assetic `: a +Symfony originally shipped with support for :doc:`Assetic `: a pure PHP library capable of processing, combining and minifying CSS and JavaScript files. And while Encore is now the recommended way of processing your assets, Assetic still works well. diff --git a/frontend/encore/vuejs.rst b/frontend/encore/vuejs.rst index a92fa1b116b..ce6f69a888b 100644 --- a/frontend/encore/vuejs.rst +++ b/frontend/encore/vuejs.rst @@ -27,7 +27,7 @@ Hot Module Replacement (HMR) ---------------------------- The ``vue-loader`` supports hot module replacement: just update your code and watch -your Vue.js app update *without* a browser refresh! To activate it, just use the +your Vue.js app update *without* a browser refresh! To activate it, use the ``dev-server`` with the ``--hot`` option: .. code-block:: terminal diff --git a/getting_started/index.rst b/getting_started/index.rst index acd7ddc5dcd..a14f36f4cd2 100644 --- a/getting_started/index.rst +++ b/getting_started/index.rst @@ -8,5 +8,5 @@ Getting Started /page_creation /routing /controller - /templating + /templates /configuration diff --git a/http_cache.rst b/http_cache.rst index 394a5ccab74..7a48728d2cd 100644 --- a/http_cache.rst +++ b/http_cache.rst @@ -77,22 +77,33 @@ but is a great way to start. For details on setting up Varnish, see :doc:`/http_cache/varnish`. -Each application comes with a caching kernel (``AppCache``) that wraps the -default one (``AppKernel``). The caching Kernel *is* the reverse proxy. +To enable the proxy, first create a caching kernel:: -To enable caching, modify the code of your front controller. You can also make these -changes to ``app_dev.php`` to add caching to the ``dev`` environment:: + // src/CacheKernel.php + namespace App; - // web/app.php - use Symfony\Component\HttpFoundation\Request; + use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache; - // ... - $kernel = new AppKernel('prod', false); - $kernel->loadClassCache(); + class CacheKernel extends HttpCache + { + } + +Modify the code of your front controller to wrap the default kernel into the +caching kernel: - // add (or uncomment) this new line! - // wrap the default AppKernel with the AppCache one - $kernel = new AppCache($kernel); +.. code-block:: diff + + // public/index.php + + + use App\CacheKernel; + use App\Kernel; + + // ... + $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); + + // Wrap the default Kernel with the CacheKernel one in 'prod' environment + + if ('prod' === $kernel->getEnvironment()) { + + $kernel = new CacheKernel($kernel); + + } $request = Request::createFromGlobals(); // ... @@ -114,15 +125,17 @@ from your application and returning them to the client. error_log($kernel->getLog()); -The ``AppCache`` object has a sensible default configuration, but it can be +The ``CacheKernel`` object has a sensible default configuration, but it can be finely tuned via a set of options you can set by overriding the :method:`Symfony\\Bundle\\FrameworkBundle\\HttpCache\\HttpCache::getOptions` method:: - // app/AppCache.php + // src/CacheKernel.php + namespace App; + use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache; - class AppCache extends HttpCache + class CacheKernel extends HttpCache { protected function getOptions() { @@ -136,10 +149,23 @@ method:: For a full list of the options and their meaning, see the :method:`HttpCache::__construct() documentation `. -When you're in debug mode (either because your booting a ``debug`` kernel, like -in ``app_dev.php`` *or* you manually set the ``debug`` option to true), Symfony -automatically adds an ``X-Symfony-Cache`` header to the response. Use this to get -information about cache hits and misses. +When you're in debug mode (the second argument of ``Kernel`` constructor in the +front controller is ``true``), Symfony automatically adds an ``X-Symfony-Cache`` +header to the response. You can also use the ``trace_level`` config +option and set it to either ``none``, ``short`` or ``full`` to +add this information. + +``short`` will add the information for the master request only. +It's written in a concise way that makes it easy to record the +information in your server log files. For example, in Apache you can +use ``%{X-Symfony-Cache}o`` in ``LogFormat`` format statements. +This information can be used to extract general information about +cache efficiency of your routes. + +.. tip:: + + You can change the name of the header used for the trace + information using the ``trace_header`` config option. .. _http-cache-symfony-versus-varnish: @@ -209,11 +235,11 @@ Expiration Caching The *easiest* way to cache a response is by caching it for a specific amount of time:: - // src/AppBundle/Controller/BlogController.php + // src/Controller/BlogController.php use Symfony\Component\HttpFoundation\Response; // ... - public function indexAction() + public function index() { // somehow create a Response object, like by rendering a template $response = $this->render('blog/index.html.twig', []); @@ -342,6 +368,28 @@ When pages contain dynamic parts, you may not be able to cache entire pages, but only parts of it. Read :doc:`/http_cache/esi` to find out how to configure different cache strategies for specific parts of your page. +HTTP Caching and User Sessions +------------------------------ + +Whenever the session is started during a request, Symfony turns the response +into a private non-cacheable response. This is the best default behavior to not +cache private user information (e.g. a shopping cart, a user profile details, +etc.) and expose it to other visitors. + +However, even requests making use of the session can be cached under some +circumstances. For example, information related to some user group could be +cached for all the users belonging to that group. Handling these advanced +caching scenarios is out of the scope of Symfony, but they can be solved with +the `FOSHttpCacheBundle`_. + +In order to disable the default Symfony behavior that makes requests using the +session uncacheable, add the following internal header to your response and +Symfony won't modify it:: + + use Symfony\Component\HttpKernel\EventListener\AbstractSessionListener; + + $response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 'true'); + Summary ------- diff --git a/http_cache/_expiration-and-validation.rst.inc b/http_cache/_expiration-and-validation.rst.inc index 7ee62604533..3ae2113e242 100644 --- a/http_cache/_expiration-and-validation.rst.inc +++ b/http_cache/_expiration-and-validation.rst.inc @@ -1,10 +1,10 @@ .. sidebar:: Expiration and Validation You can use both validation and expiration within the same ``Response``. - As expiration wins over validation, you can 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. + As expiration wins over validation, you can 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. .. tip:: diff --git a/http_cache/cache_invalidation.rst b/http_cache/cache_invalidation.rst index 764ccd09610..8cb1b6fb80e 100644 --- a/http_cache/cache_invalidation.rst +++ b/http_cache/cache_invalidation.rst @@ -9,7 +9,7 @@ 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 +Once a 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 @@ -47,16 +47,18 @@ 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:: +Here is how you can configure the Symfony reverse proxy (See :doc:`/http_cache`) +to support the ``PURGE`` HTTP method:: + + // src/CacheKernel.php + namespace App; - // app/AppCache.php use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; // ... - class AppCache extends HttpCache + class CacheKernel extends HttpCache { protected function invalidate(Request $request, $catch = false) { diff --git a/http_cache/esi.rst b/http_cache/esi.rst index 24c8341f048..f8dcf6702fb 100644 --- a/http_cache/esi.rst +++ b/http_cache/esi.rst @@ -62,16 +62,16 @@ First, to use ESI, be sure to enable it in your application configuration: .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: # ... esi: { enabled: true } .. code-block:: xml - + - loadFromExtension('framework', [ // ... 'fragments' => ['path' => '/_fragment'], diff --git a/http_cache/form_csrf_caching.rst b/http_cache/form_csrf_caching.rst deleted file mode 100644 index 6e031cc5be9..00000000000 --- a/http_cache/form_csrf_caching.rst +++ /dev/null @@ -1,40 +0,0 @@ -.. index:: - single: Cache; CSRF; Forms - -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 :doc:`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 behavior 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 :doc:`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. Take a look at :doc:`HInclude ` -for a nice solution. diff --git a/http_cache/ssi.rst b/http_cache/ssi.rst new file mode 100644 index 00000000000..44ebda2990c --- /dev/null +++ b/http_cache/ssi.rst @@ -0,0 +1,141 @@ +.. index:: + single: Cache; SSI + single: SSI + +.. _server-side-includes: + +Working with Server Side Includes +================================= + +In a similar way as :doc:`ESI (Edge Side Includes) `, +SSI can be used to control HTTP caching on fragments of a response. +The most important difference that is SSI is known directly by most +web servers like `Apache`_, `Nginx`_ etc. + +The SSI instructions are done via HTML comments: + +.. code-block:: html + + + + + + + + + + + + + +There are some other `available directives`_ but +Symfony manages only the ``#include virtual`` one. + +.. caution:: + + Be careful with SSI, your website may be victim of injections. + Please read this `OWASP article`_ first! + +When the web server reads an SSI directive, it requests the given URI or gives +directly from its cache. It repeats this process until there is no more +SSI directives to handle. Then, it merges all responses into one and sends +it to the client. + +.. _using-ssi-in-symfony: + +Using SSI in Symfony +~~~~~~~~~~~~~~~~~~~~ + +First, to use SSI, be sure to enable it in your application configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + ssi: { enabled: true } + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + $container->loadFromExtension('framework', [ + 'ssi' => ['enabled' => true], + ]); + +Suppose you have a page with private content like a Profile page and you want +to cache a static GDPR content block. With SSI, you can add some expiration +on this block and keep the page private:: + + // src/Controller/ProfileController.php + + // ... + class ProfileController extends AbstractController + { + public function index(): Response + { + // by default, responses are private + return $this->render('profile/index.html.twig'); + } + + public function gdpr(): Response + { + $response = $this->render('profile/gdpr.html.twig'); + + // sets to public and adds some expiration + $response->setSharedMaxAge(600); + + return $response; + } + } + +The profile index page has not public caching, but the GDPR block has +10 minutes of expiration. Let's include this block into the main one: + +.. code-block:: twig + + {# templates/profile/index.html.twig #} + + {# you can use a controller reference #} + {{ render_ssi(controller('App\Controller\ProfileController::gdpr')) }} + + {# ... or a URL #} + {{ render_ssi(url('profile_gdpr')) }} + +The ``render_ssi`` twig helper will generate something like: + +.. code-block:: html + + + +``render_ssi`` ensures that SSI directive are generated only if the request +has the header requirement like ``Surrogate-Capability: device="SSI/1.0"`` +(normally given by the web server). +Otherwise it will embed directly the sub-response. + +.. note:: + + For more information about Symfony cache fragments, take a tour on + the :ref:`ESI documentation `. + +.. _`Apache`: https://httpd.apache.org/docs/current/en/howto/ssi.html +.. _`Nginx`: https://nginx.org/en/docs/http/ngx_http_ssi_module.html +.. _`available directives`: https://en.wikipedia.org/wiki/Server_Side_Includes#Directives +.. _`OWASP article`: https://www.owasp.org/index.php/Server-Side_Includes_(SSI)_Injection diff --git a/http_cache/validation.rst b/http_cache/validation.rst index 68f0ce1c9cf..24d3711cfe5 100644 --- a/http_cache/validation.rst +++ b/http_cache/validation.rst @@ -23,8 +23,8 @@ 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 a light-weight set of directions that - tells the cache that it should use its stored version. + requested. Instead, the response only consists of the response headers that + tells the cache that it can use its stored version of the content. Like with expiration, there are two different HTTP headers that can be used to implement the validation model: ``ETag`` and ``Last-Modified``. @@ -51,15 +51,15 @@ different versions of a resource are equivalent. Like fingerprints, each To see a simple implementation, generate the ``ETag`` as the ``md5`` of the content:: - // src/AppBundle/Controller/DefaultController.php - namespace AppBundle\Controller; + // src/Controller/DefaultController.php + namespace App\Controller; - use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; - class DefaultController extends Controller + class DefaultController extends AbstractController { - public function homepageAction(Request $request) + public function homepage(Request $request) { $response = $this->render('static/homepage.html.twig'); $response->setEtag(md5($response->getContent())); @@ -128,17 +128,17 @@ 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:: - // src/AppBundle/Controller/ArticleController.php - namespace AppBundle\Controller; + // src/Controller/ArticleController.php + namespace App\Controller; // ... - use AppBundle\Entity\Article; + use App\Entity\Article; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; - class ArticleController extends Controller + class ArticleController extends AbstractController { - public function showAction(Article $article, Request $request) + public function show(Article $article, Request $request) { $author = $article->getAuthor(); @@ -188,16 +188,16 @@ 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:: - // src/AppBundle/Controller/ArticleController.php - namespace AppBundle\Controller; + // src/Controller/ArticleController.php + namespace App\Controller; // ... use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; - class ArticleController extends Controller + class ArticleController extends AbstractController { - public function showAction($articleSlug, Request $request) + public function show($articleSlug, Request $request) { // Get the minimum information to compute // the ETag or the Last-Modified value diff --git a/http_cache/varnish.rst b/http_cache/varnish.rst index a120049b8e0..776097339f9 100644 --- a/http_cache/varnish.rst +++ b/http_cache/varnish.rst @@ -62,10 +62,10 @@ 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 -:doc:`CSRF Protection `. In this situation, make sure to -:doc:`only start a session when actually needed ` +:doc:`CSRF Protection `. In this situation, make sure to +:ref:`only start a session when actually needed ` and clear the session when it is no longer needed. Alternatively, you can look -into :doc:`/http_cache/form_csrf_caching`. +into :ref:`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 @@ -210,7 +210,7 @@ Symfony adds automatically: .. tip:: If you followed the advice about ensuring a consistent caching - behavior, those VCL functions already exist. Just append the code + behavior, those VCL functions already exist. Append the code to the end of the function, they won't interfere with each other. .. index:: diff --git a/index.rst b/index.rst index 4e64ed04374..ac293625cb9 100644 --- a/index.rst +++ b/index.rst @@ -14,8 +14,7 @@ Get started fast with the Symfony :doc:`Quick Tour `: quick_tour/index * :doc:`quick_tour/the_big_picture` -* :doc:`quick_tour/the_view` -* :doc:`quick_tour/the_controller` +* :doc:`quick_tour/flex_recipes` * :doc:`quick_tour/the_architecture` Getting Started @@ -32,11 +31,11 @@ Topics .. toctree:: :maxdepth: 1 + best_practices bundles cache console doctrine - debug deployment email event_dispatcher @@ -44,6 +43,10 @@ Topics frontend http_cache logging + mailer + mercure + messenger + migration performance profiler routing @@ -58,16 +61,6 @@ Topics web_link workflow -Best Practices --------------- - -.. toctree:: - :hidden: - - best_practices/index - -Read the :doc:`Official Best Practices `. - Components ---------- diff --git a/introduction/from_flat_php_to_symfony.rst b/introduction/from_flat_php_to_symfony.rst index 13b61f7eb79..225ff8cdeba 100644 --- a/introduction/from_flat_php_to_symfony.rst +++ b/introduction/from_flat_php_to_symfony.rst @@ -128,7 +128,7 @@ is primarily an HTML file that uses a template-like PHP syntax: By convention, the file that contains all the application logic - ``index.php`` - is known as a "controller". The term controller is a word you'll hear a lot, -regardless of the language or framework you use. It refers simply to the area +regardless of the language or framework you use. It refers to the area of *your* code that processes user input and prepares the response. In this case, the controller prepares data from the database and then includes @@ -181,7 +181,7 @@ of the application are isolated in a new file called ``model.php``:: in this example, only a portion (or none) of the model is actually concerned with accessing a database. -The controller (``index.php``) is now very simple:: +The controller (``index.php``) is now just a few lines of code:: // index.php require_once 'model.php'; @@ -192,7 +192,7 @@ The controller (``index.php``) is now very simple:: Now, the sole task of the controller is to get data from the model layer of the application (the model) and to call a template to render that data. -This is a very simple example of the model-view-controller pattern. +This is a very concise example of the model-view-controller pattern. Isolating the Layout ~~~~~~~~~~~~~~~~~~~~ @@ -261,7 +261,7 @@ an individual blog result based on a given id:: { $connection = open_database_connection(); - $query = 'SELECT created_at, title, body FROM post WHERE id=:id'; + $query = 'SELECT created_at, title, body FROM post WHERE id=:id'; $statement = $connection->prepare($query); $statement->bindValue(':id', $id, PDO::PARAM_INT); $statement->execute(); @@ -302,7 +302,7 @@ the individual blog post: -Creating the second page is now very easy and no code is duplicated. Still, +Creating the second page now requires very little work and no code is duplicated. Still, this page introduces even more lingering problems that a framework can solve for you. For example, a missing or invalid ``id`` query parameter will cause the page to crash. It would be better if this caused a 404 page to be rendered, @@ -429,7 +429,7 @@ content: { "require": { - "symfony/symfony": "3.1.*" + "symfony/http-foundation": "^4.0" }, "autoload": { "files": ["model.php","controllers.php"] @@ -532,32 +532,30 @@ a simple application. Along the way, you've made a simple routing system and a method using ``ob_start()`` and ``ob_get_clean()`` to render templates. If, for some reason, you needed to continue building this "framework" from scratch, you could at least use Symfony's standalone -:doc:`Routing ` and -:doc:`Templating ` components, which already -solve these problems. +:doc:`Routing ` component and :doc:`Twig `, +which already solve these problems. Instead of re-solving common problems, you can let Symfony take care of them for you. Here's the same sample application, now built in Symfony:: - // src/AppBundle/Controller/BlogController.php - namespace AppBundle\Controller; + // src/Controller/BlogController.php + namespace App\Controller; - use AppBundle\Entity\Post; - use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use App\Entity\Post; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - class BlogController extends Controller + class BlogController extends AbstractController { - public function listAction() + public function list() { $posts = $this->getDoctrine() - ->getManager() - ->createQuery('SELECT p FROM AppBundle:Post p') - ->execute(); + ->getRepository(Post::class) + ->findAll(); - return $this->render('Blog/list.html.php', ['posts' => $posts]); + return $this->render('blog/list.html.twig', ['posts' => $posts]); } - public function showAction($id) + public function show($id) { $post = $this->getDoctrine() ->getRepository(Post::class) @@ -568,7 +566,7 @@ them for you. Here's the same sample application, now built in Symfony:: throw $this->createNotFoundException(); } - return $this->render('Blog/show.html.php', ['post' => $post]); + return $this->render('blog/show.html.twig', ['post' => $post]); } } @@ -579,79 +577,79 @@ nice way to group related pages. The controller functions are also sometimes cal The two controllers (or actions) are still lightweight. Each uses the :doc:`Doctrine ORM library ` to retrieve objects from the database and the Templating component to render a template and return a -``Response`` object. The ``list.php`` template is now quite a bit simpler: +``Response`` object. The ``list.html.twig`` template is now quite a bit simpler, +and uses Twig: -.. code-block:: html+php +.. code-block:: html+twig - - extend('layout.html.php') ?> + + {% extends 'base.html.twig' %} - set('title', 'List of Posts') ?> + {% block title %}List of Posts{% endblock %} + {% block body %}

List of Posts

+ {% endblock %} The ``layout.php`` file is nearly identical: -.. code-block:: html+php +.. code-block:: html+twig - + - <?= $view['slots']->output( - 'title', - 'Default title' - ) ?> + + {% block title %}Welcome!{% endblock %} + {% block stylesheets %}{% endblock %} - output('_content') ?> + {% block body %}{% endblock %} + {% block javascripts %}{% endblock %} .. note:: - The ``show.php`` template is left as an exercise: updating it should be - really similar to updating the ``list.php`` template. + The ``show.html.twig`` template is left as an exercise: updating it should be + really similar to updating the ``list.html.twig`` template. When Symfony's engine (called the Kernel) boots up, it needs a map so that it knows which controllers to execute based on the request information. -A routing configuration map - ``app/config/routing.yml`` - provides this information +A routing configuration map - ``config/routes.yaml`` - provides this information in a readable format: .. code-block:: yaml - # app/config/routing.yml + # config/routes.yaml blog_list: path: /blog - defaults: { _controller: AppBundle:Blog:list } + controller: App\Controller\BlogController::list blog_show: path: /blog/show/{id} - defaults: { _controller: AppBundle:Blog:show } + controller: App\Controller\BlogController::show Now that Symfony is handling all the mundane tasks, the front controller -``web/app.php`` is dead simple. And since it does so little, you'll never +``public/index.php`` is reduced to bootstrapping. And since it does so little, you'll never have to touch it:: - // web/app.php + // public/index.php require_once __DIR__.'/../app/bootstrap.php'; - require_once __DIR__.'/../app/AppKernel.php'; + require_once __DIR__.'/../src/Kernel.php'; use Symfony\Component\HttpFoundation\Request; - $kernel = new AppKernel('prod', false); + $kernel = new Kernel('prod', false); $kernel->handle(Request::createFromGlobals())->send(); The front controller's only job is to initialize Symfony's engine (called the @@ -665,57 +663,9 @@ object are sent back to the client. It's a beautiful thing. -.. figure:: /_images/http/request-flow.png - :align: center - :alt: Symfony request flow - -Better Templates -~~~~~~~~~~~~~~~~ - -If you choose to use it, Symfony comes with a templating engine -called `Twig`_ that makes templates faster to write and easier to read. -It means that the sample application could contain even less code! For -example, rewriting the ``list.html.php`` template in Twig would look like -this: - -.. code-block:: html+twig - - {# app/Resources/views/blog/list.html.twig #} - {% extends "layout.html.twig" %} - - {% block title %}List of Posts{% endblock %} - - {% block body %} -

List of Posts

- - {% endblock %} - -And rewriting the ``layout.html.php`` template in Twig would look like this: - -.. code-block:: html+twig - - {# app/Resources/views/layout.html.twig #} - - - - {% block title %}Default title{% endblock %} - - - {% block body %}{% endblock %} - - +.. raw:: html -Twig is well-supported in Symfony. And while PHP templates will always -be supported in Symfony, the many advantages of Twig will continue to -be discussed. For more information, see the :doc:`templating article `. + Where Symfony Delivers ---------------------- @@ -758,5 +708,4 @@ A good selection of `Symfony community tools`_ can be found on GitHub. .. _`download Composer`: https://getcomposer.org/download/ .. _`Validator`: https://github.com/symfony/validator .. _`Varnish`: https://www.varnish-cache.org/ -.. _`Twig`: https://twig.symfony.com .. _`Symfony community tools`: https://github.com/search?q=topic%3Asymfony-bundle&type=Repositories diff --git a/introduction/http_fundamentals.rst b/introduction/http_fundamentals.rst index f0cf5cc948a..11fc83efadf 100644 --- a/introduction/http_fundamentals.rst +++ b/introduction/http_fundamentals.rst @@ -56,7 +56,7 @@ In HTTP-speak, this HTTP request would actually look something like this: Accept: text/html User-Agent: Mozilla/5.0 (Macintosh) -This simple message communicates *everything* necessary about exactly which +These few lines communicate *everything* necessary about exactly which resource the client is requesting. The first line of an HTTP request is the most important, because it contains two important things: the HTTP method (GET) and the URI (``/``). @@ -243,7 +243,7 @@ Symfony Response Object ~~~~~~~~~~~~~~~~~~~~~~~ Symfony also provides a :class:`Symfony\\Component\\HttpFoundation\\Response` -class: a simple PHP representation of an HTTP response message. This allows your +class: a 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:: @@ -283,7 +283,7 @@ The Journey from the Request to the Response -------------------------------------------- Like HTTP itself, using the ``Request`` and ``Response`` objects is pretty -simple. The hard part of building an application is writing what comes in +straightforward. 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. @@ -359,12 +359,12 @@ to do: .. _request-flow-figure: -.. figure:: /_images/http/request-flow.png - :align: center - :alt: Symfony request flow +.. raw:: html + + - Incoming requests are interpreted by the :doc:`Routing component ` and - passed to PHP functions that return ``Response`` objects. +Incoming requests are interpreted by the :doc:`Routing component ` and +passed to PHP functions that return ``Response`` objects. This may not make sense yet, but as you keep reading, you'll learn about :doc:`routes ` and :doc:`controllers `: the two fundamental parts to creating a page. diff --git a/logging.rst b/logging.rst index af5b72f2ba9..61471ec6bcb 100644 --- a/logging.rst +++ b/logging.rst @@ -27,11 +27,8 @@ To log a message, inject the default logger in your controller:: use Psr\Log\LoggerInterface; - public function indexAction(LoggerInterface $logger) + public function index(LoggerInterface $logger) { - // alternative way of getting the logger - // $logger = $this->get('logger'); - $logger->info('I just got the logger'); $logger->error('An error occurred'); @@ -67,12 +64,8 @@ The following sections assume that Monolog is installed. Where Logs are Stored --------------------- -The configuration for *where* logs are stored lives in the specific -:doc:`environment ` configuration files: ``config_dev.yml`` -and ``config_prod.yml``. - -By default, log entries are written to the ``var/logs/dev.log`` file when you're in -the ``dev`` environment. In the ``prod`` environment, logs are written to ``var/logs/prod.log``, +By default, log entries are written to the ``var/log/dev.log`` file when you're in +the ``dev`` environment. In the ``prod`` environment, logs are written to ``var/log/prod.log``, but *only* during a request where an error or high-priority log entry was made (i.e. ``error()`` , ``critical()``, ``alert()`` or ``emergency()``). @@ -91,8 +84,8 @@ to different locations (e.g. files, database, Slack, etc). channel can have its *own* handlers, which means you can store different log messages in different places. See :doc:`/logging/channels_handlers`. -Symfony pre-configures some basic handlers in the ``config_dev.yml`` and ``config_prod.yml`` -files. Check these out for some real-world examples. +Symfony pre-configures some basic handlers in the default ``monolog.yaml`` +config files. Check these out for some real-world examples. This example uses *two* handlers: ``stream`` (to write to a file) and ``syslog`` to write logs using the :phpfunction:`syslog` function: @@ -101,13 +94,13 @@ to write logs using the :phpfunction:`syslog` function: .. code-block:: yaml - # app/config/config.yml + # config/packages/prod/monolog.yaml monolog: handlers: # this "file_log" key could be anything file_log: type: stream - # log to var/logs/(environment).log + # log to var/log/(environment).log path: "%kernel.logs_dir%/%kernel.environment%.log" # log *all* messages (debug is lowest level) level: debug @@ -119,7 +112,7 @@ to write logs using the :phpfunction:`syslog` function: .. code-block:: xml - + loadFromExtension('monolog', [ 'handlers' => [ 'file_log' => [ @@ -177,7 +170,7 @@ one of the messages reaches an ``action_level``. Take this example: .. code-block:: yaml - # app/config/config.yml + # config/packages/prod/monolog.yaml monolog: handlers: filter_for_errors: @@ -198,7 +191,7 @@ one of the messages reaches an ``action_level``. Take this example: .. code-block:: xml - + loadFromExtension('monolog', [ 'handlers' => [ 'filter_for_errors' => [ @@ -284,14 +277,14 @@ Linux command to rotate log files before they become too large. Another option is to have Monolog rotate the files for you by using the ``rotating_file`` handler. This handler creates a new log file every day -and can also remove old files automatically. To use it, just set the ``type`` +and can also remove old files automatically. To use it, set the ``type`` option of your handler to ``rotating_file``: .. configuration-block:: .. code-block:: yaml - # app/config/config_dev.yml + # config/packages/prod/monolog.yaml monolog: handlers: main: @@ -304,7 +297,7 @@ option of your handler to ``rotating_file``: .. code-block:: xml - + loadFromExtension('monolog', [ 'handlers' => [ 'main' => [ @@ -345,10 +338,14 @@ option of your handler to ``rotating_file``: Using a Logger inside a Service ------------------------------- -To use a logger in your own services, add the ``@logger`` service as an argument -of those services. If you want to use a pre-configured logger which uses a -specific channel (``app`` by default), use the ``monolog.logger`` tag with the -``channel`` property as explained in the +If your application uses :ref:`service autoconfiguration `, +any service whose class implements ``Psr\Log\LoggerAwareInterface`` will +receive a call to its method ``setLogger()`` with the default logger service +passed as a service. + +If you want to use in your own services a pre-configured logger which uses a +specific channel (``app`` by default), you can either :ref:`autowire monolog channels ` +or use the ``monolog.logger`` tag with the ``channel`` property as explained in the :ref:`Dependency Injection reference `. Adding extra Data to each Log (e.g. a unique request token) @@ -364,9 +361,19 @@ Learn more .. toctree:: :maxdepth: 1 - :glob: - logging/* + logging/monolog_email + logging/channels_handlers + logging/formatter + logging/processors + logging/handlers + logging/monolog_exclude_http_codes + logging/monolog_console + +.. toctree:: + :hidden: + + logging/monolog_regex_based_excludes .. _`the twelve-factor app methodology`: https://12factor.net/logs .. _PSR-3: https://www.php-fig.org/psr/psr-3/ diff --git a/logging/channels_handlers.rst b/logging/channels_handlers.rst index 92d35c073c3..76f671e8ed3 100644 --- a/logging/channels_handlers.rst +++ b/logging/channels_handlers.rst @@ -14,9 +14,9 @@ the channel). .. note:: - Each channel corresponds to a logger service (``monolog.logger.XXX``) - in the container (use the ``php bin/console debug:container monolog`` command - to see a full list) and those are injected into different services. + Each channel corresponds to a different logger service (``monolog.logger.XXX``) + Use the ``php bin/console debug:container monolog`` command to see a full + list of services and learn :ref:`how to autowire monolog channels `. .. _logging-channel-handler: @@ -24,15 +24,14 @@ Switching a Channel to a different Handler ------------------------------------------ Now, suppose you want to log the ``security`` channel to a different file. -To do this, just create a new handler and configure it to log only messages -from the ``security`` channel. You might add this in ``config.yml`` to log -in all environments, or just ``config_prod.yml`` to happen only in ``prod``: +To do this, create a new handler and configure it to log only messages +from the ``security`` channel: .. configuration-block:: .. code-block:: yaml - # app/config/config.yml + # config/packages/prod/monolog.yaml monolog: handlers: security: @@ -49,7 +48,7 @@ in all environments, or just ``config_prod.yml`` to happen only in ``prod``: .. code-block:: xml - + loadFromExtension('monolog', [ 'handlers' => [ 'security' => [ @@ -138,13 +137,13 @@ You can also configure additional channels without the need to tag your services .. code-block:: yaml - # app/config/config.yml + # config/packages/prod/monolog.yaml monolog: channels: ['foo', 'bar'] .. code-block:: xml - + loadFromExtension('monolog', [ 'channels' => [ 'foo', @@ -173,3 +172,23 @@ Symfony automatically registers one service per channel (in this example, the channel ``foo`` creates a service called ``monolog.logger.foo``). In order to inject this service into others, you must update the service configuration to :ref:`choose the specific service to inject `. + +.. _monolog-autowire-channels: + +How to Autowire Logger Channels +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Starting from `MonologBundle`_ 3.5 you can autowire different Monolog channels +by type-hinting your service arguments with the following syntax: +``Psr\Log\LoggerInterface $Logger``. For example, to inject the service +related to the ``app`` logger channel use this: + +.. code-block:: diff + + - public function __construct(LoggerInterface $logger) + + public function __construct(LoggerInterface $appLogger) + { + $this->logger = $appLogger; + } + +.. _`MonologBundle`: https://github.com/symfony/monolog-bundle diff --git a/logging/disable_microsecond_precision.rst b/logging/disable_microsecond_precision.rst deleted file mode 100644 index 6676bd1bc81..00000000000 --- a/logging/disable_microsecond_precision.rst +++ /dev/null @@ -1,57 +0,0 @@ -How to Disable Microseconds Precision (for a Performance Boost) -=============================================================== - -Setting the parameter ``use_microseconds`` to ``false`` forces the logger to reduce -the precision in the ``datetime`` field of the log messages from microsecond to second, -avoiding a call to the ``microtime(true)`` function and the subsequent parsing. -Disabling the use of microseconds can provide a small performance gain speeding up the -log generation. This is recommended for systems that generate a large number of log events. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - monolog: - use_microseconds: false - handlers: - applog: - type: stream - path: /var/log/symfony.log - level: error - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('monolog', [ - 'use_microseconds' => false, - 'handlers' => [ - 'applog' => [ - 'type' => 'stream', - 'path' => '/var/log/symfony.log', - 'level' => 'error', - ], - ], - ]); diff --git a/logging/formatter.rst b/logging/formatter.rst index d80359f9b4b..b41cd7ad06e 100644 --- a/logging/formatter.rst +++ b/logging/formatter.rst @@ -13,7 +13,7 @@ configure your handler to use it: .. code-block:: yaml - # app/config/config_prod.yml (and/or config_dev.yml) + # config/packages/prod/monolog.yaml (and/or config/packages/dev/monolog.yaml) monolog: handlers: file: @@ -23,7 +23,7 @@ configure your handler to use it: .. code-block:: xml - + - + loadFromExtension('monolog', [ 'handlers' => [ 'file' => [ diff --git a/logging/handlers.rst b/logging/handlers.rst new file mode 100644 index 00000000000..9f5b903cfa9 --- /dev/null +++ b/logging/handlers.rst @@ -0,0 +1,101 @@ +Handlers +======== + +ElasticsearchLogstashHandler +---------------------------- + +This handler deals directly with the HTTP interface of Elasticsearch. This means +it will slow down your application if Elasticsearch takes times to answer. Even +if all HTTP calls are done asynchronously. + +In a development environment, it's fine to keep the default configuration: for +each log, an HTTP request will be made to push the log to Elasticsearch. + +In a production environment, it's highly recommended to wrap this handler in a +handler with buffering capabilities (like the ``FingersCrossedHandler`` or +``BufferHandler``) in order to call Elasticsearch only once with a bulk push. For +even better performance and fault tolerance, a proper `ELK stack`_ is recommended. + +To use it, declare it as a service: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler: ~ + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/services.php + use Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler; + + $container->register(ElasticsearchLogstashHandler::class); + +Then reference it in the Monolog configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/prod/monolog.yaml + monolog: + handlers: + es: + type: service + id: Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/prod/monolog.php + use Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler; + + $container->loadFromExtension('monolog', [ + 'handlers' => [ + 'es' => [ + 'type' => 'service', + 'id' => ElasticsearchLogstashHandler::class, + ], + ], + ]); + +.. _`ELK stack`: https://www.elastic.co/what-is/elk-stack diff --git a/logging/monolog_console.rst b/logging/monolog_console.rst index ab9132ee83d..bd41a61a53a 100644 --- a/logging/monolog_console.rst +++ b/logging/monolog_console.rst @@ -34,16 +34,27 @@ current log level and the console verbosity. The example above could then be rewritten as:: + use Psr\Log\LoggerInterface; + use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; + // ... - protected function execute(InputInterface $input, OutputInterface $output) + class YourCommand extends Command { - // assuming the Command extends ContainerAwareCommand... - $logger = $this->getContainer()->get('logger'); - $logger->debug('Some info'); + private $logger; + + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } - $logger->notice('Some more info'); + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->logger->debug('Some info'); + // ... + $this->logger->notice('Some more info'); + } } Depending on the verbosity level that the command is run in and the user's @@ -52,14 +63,13 @@ the console. If they are displayed, they are timestamped and colored appropriate Additionally, error logs are written to the error output (``php://stderr``). There is no need to conditionally handle the verbosity settings anymore. -The Monolog console handler is enabled by default in the Symfony Framework. For -example, in ``config_dev.yml``: +The Monolog console handler is enabled by default: .. configuration-block:: .. code-block:: yaml - # app/config/config_dev.yml + # config/packages/dev/monolog.yaml monolog: handlers: # ... @@ -74,7 +84,7 @@ example, in ``config_dev.yml``: .. code-block:: xml - + loadFromExtension('monolog', [ 'handlers' => [ 'console' => [ - 'type' => 'console', - 'process_psr_3_messages' => false, - 'channels' => ['!event', '!doctrine', '!console'], + 'type' => 'console', + 'process_psr_3_messages' => false, + 'channels' => ['!event', '!doctrine', '!console'], ], ], ]); diff --git a/logging/monolog_email.rst b/logging/monolog_email.rst index 70cb7bce745..9e824a77def 100644 --- a/logging/monolog_email.rst +++ b/logging/monolog_email.rst @@ -4,6 +4,11 @@ How to Configure Monolog to Email Errors ======================================== +.. caution:: + + This feature is not compatible yet with the new :doc:`Symfony mailer `, + so it requires using SwiftMailer. + `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 @@ -14,7 +19,7 @@ it is broken down. .. code-block:: yaml - # app/config/config_prod.yml + # config/packages/prod/monolog.yaml monolog: handlers: main: @@ -42,7 +47,7 @@ it is broken down. .. code-block:: xml - + loadFromExtension('monolog', [ 'handlers' => [ 'main' => [ @@ -149,7 +154,7 @@ You can adjust the time period using the ``time`` option: .. code-block:: yaml - # app/config/config_prod.yml + # config/packages/prod/monolog.yaml monolog: handlers: # ... @@ -161,7 +166,7 @@ You can adjust the time period using the ``time`` option: .. code-block:: xml - + loadFromExtension('monolog', [ 'handlers' => [ // ... @@ -196,7 +201,7 @@ get logged on the server as well as the emails being sent: .. code-block:: yaml - # app/config/config_prod.yml + # config/packages/prod/monolog.yaml monolog: handlers: main: @@ -224,7 +229,7 @@ get logged on the server as well as the emails being sent: .. code-block:: xml - + loadFromExtension('monolog', [ 'handlers' => [ 'main' => [ diff --git a/logging/monolog_exclude_http_codes.rst b/logging/monolog_exclude_http_codes.rst new file mode 100644 index 00000000000..9c1bd81bdcc --- /dev/null +++ b/logging/monolog_exclude_http_codes.rst @@ -0,0 +1,69 @@ +.. index:: + single: Logging + single: Logging; Exclude HTTP Codes + single: Monolog; Exclude HTTP Codes + +How to Configure Monolog to Exclude Specific HTTP Codes from the Log +==================================================================== + +Sometimes your logs become flooded with unwanted HTTP errors, for example, +403s and 404s. When using a ``fingers_crossed`` handler, you can exclude +logging these HTTP codes based on the MonologBundle configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/prod/monolog.yaml + monolog: + handlers: + main: + # ... + type: fingers_crossed + handler: ... + excluded_http_codes: [403, 404, { 400: ['^/foo', '^/bar'] }] + + .. code-block:: xml + + + + + + + + + ^/foo + ^/bar + + + + + + + .. code-block:: php + + // config/packages/prod/monolog.php + $container->loadFromExtension('monolog', [ + 'handlers' => [ + 'main' => [ + // ... + 'type' => 'fingers_crossed', + 'handler' => ..., + 'excluded_http_codes' => [403, 404], + ], + ], + ]); + +.. caution:: + + Combining ``excluded_http_codes`` with a ``passthru_level`` lower than + ``error`` (i.e. ``debug``, ``info``, ``notice`` or ``warning``) will not + actually exclude log messages for those HTTP codes because they are logged + with level of ``error`` or higher and ``passthru_level`` takes precedence + over the HTTP codes being listed in ``excluded_http_codes``. diff --git a/logging/monolog_regex_based_excludes.rst b/logging/monolog_regex_based_excludes.rst index 19f0e51421d..be4a6ee8b7e 100644 --- a/logging/monolog_regex_based_excludes.rst +++ b/logging/monolog_regex_based_excludes.rst @@ -6,6 +6,12 @@ How to Configure Monolog to Exclude 404 Errors from the Log =========================================================== +.. tip:: + + Read :doc:`/logging/monolog_exclude_http_codes` to learn about a similar + but more generic feature that allows to exclude logs for any HTTP status + code and not only 404 errors. + 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 @@ -16,7 +22,7 @@ configuration: .. code-block:: yaml - # app/config/config.yml + # config/packages/prod/monolog.yaml monolog: handlers: main: @@ -28,7 +34,7 @@ configuration: .. code-block:: xml - + loadFromExtension('monolog', [ 'handlers' => [ 'main' => [ diff --git a/logging/processors.rst b/logging/processors.rst index 49645027729..bf0aeb854fe 100644 --- a/logging/processors.rst +++ b/logging/processors.rst @@ -1,9 +1,9 @@ How to Add extra Data to Log Messages via a Processor ===================================================== -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. +`Monolog`_ allows you to process every record before logging it by adding some +extra data. This is the role of a processor, which can be applied for the whole +handler stack or only for a specific handler or channel. A processor is a callable receiving the record as its first argument. Processors are configured using the ``monolog.processor`` DIC tag. See the @@ -16,7 +16,7 @@ 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 AppBundle\Logger; + namespace App\Logger; use Symfony\Component\HttpFoundation\Session\SessionInterface; @@ -30,6 +30,7 @@ using a processor:: $this->session = $session; } + // this method is called for each log record; optimize it to not hurt performance public function __invoke(array $record) { if (!$this->session->isStarted()) { @@ -53,21 +54,20 @@ information: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: monolog.formatter.session_request: class: Monolog\Formatter\LineFormatter arguments: - "[%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%% %%context%% %%extra%%\n" - AppBundle\Logger\SessionRequestProcessor: - autowire: true + App\Logger\SessionRequestProcessor: tags: - { name: monolog.processor } .. code-block:: xml - + [%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%% %%context%% %%extra%% - + @@ -92,8 +92,8 @@ information: .. code-block:: php - // app/config/services.php - use AppBundle\Logger\SessionRequestProcessor; + // config/services.php + use App\Logger\SessionRequestProcessor; use Monolog\Formatter\LineFormatter; $container @@ -101,7 +101,7 @@ information: ->addArgument('[%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%% %%context%% %%extra%%\n'); $container - ->autowire(SessionRequestProcessor::class) + ->register(SessionRequestProcessor::class) ->addTag('monolog.processor', ['method' => 'processRecord']); Finally, set the formatter to be used on whatever handler you want: @@ -110,7 +110,7 @@ Finally, set the formatter to be used on whatever handler you want: .. code-block:: yaml - # app/config/config.yml + # config/packages/prod/monolog.yaml monolog: handlers: main: @@ -121,7 +121,7 @@ Finally, set the formatter to be used on whatever handler you want: .. code-block:: xml - + loadFromExtension('monolog', [ 'handlers' => [ 'main' => [ @@ -174,9 +174,16 @@ Symfony's MonologBridge provides processors that can be registered inside your a Overrides data from the request using the data inside Symfony's request object. -.. versionadded:: 3.4 +:class:`Symfony\\Bridge\\Monolog\\Processor\\RouteProcessor` + Adds information about current route (controller, action, route parameters). - The ``TokenProcessor`` class was introduced in Symfony 3.4. +:class:`Symfony\\Bridge\\Monolog\\Processor\\ConsoleCommandProcessor` + Adds information about current console command. + +.. seealso:: + + Check out the `built-in Monolog processors`_ to learn more about how to + create these processors. Registering Processors per Handler ---------------------------------- @@ -188,16 +195,15 @@ the ``monolog.processor`` tag: .. code-block:: yaml - # app/config/config.yml + # config/services.yaml services: - AppBundle\Logger\SessionRequestProcessor: - autowire: true + App\Logger\SessionRequestProcessor: tags: - { name: monolog.processor, handler: main } .. code-block:: xml - + - + @@ -216,11 +222,11 @@ the ``monolog.processor`` tag: .. code-block:: php - // app/config/config.php + // config/services.php // ... $container - ->autowire(SessionRequestProcessor::class) + ->register(SessionRequestProcessor::class) ->addTag('monolog.processor', ['handler' => 'main']); Registering Processors per Channel @@ -233,16 +239,15 @@ the ``monolog.processor`` tag: .. code-block:: yaml - # app/config/config.yml + # config/services.yaml services: - AppBundle\Logger\SessionRequestProcessor: - autowire: true + App\Logger\SessionRequestProcessor: tags: - { name: monolog.processor, channel: main } .. code-block:: xml - + - + @@ -261,9 +266,12 @@ the ``monolog.processor`` tag: .. code-block:: php - // app/config/config.php + // config/services.php // ... $container - ->autowire(SessionRequestProcessor::class) + ->register(SessionRequestProcessor::class) ->addTag('monolog.processor', ['channel' => 'main']); + +.. _`Monolog`: https://github.com/Seldaek/monolog +.. _`built-in Monolog processors`: https://github.com/Seldaek/monolog/tree/master/src/Monolog/Processor diff --git a/mailer.rst b/mailer.rst new file mode 100644 index 00000000000..d407a0568c6 --- /dev/null +++ b/mailer.rst @@ -0,0 +1,780 @@ +Sending Emails with Mailer +========================== + +Installation +------------ + +Symfony's Mailer & :doc:`Mime ` components form a *powerful* system +for creating and sending emails - complete with support for multipart messages, Twig +integration, CSS inlining, file attachments and a lot more. Get them installed with: + +.. code-block:: terminal + + $ composer require symfony/mailer + +Transport Setup +--------------- + +Emails are delivered via a "transport". And without installing anything else, you +can deliver emails over ``smtp`` by configuring your ``.env`` file: + +.. code-block:: bash + + # .env + MAILER_DSN=smtp://user:pass@smtp.example.com + +.. warning:: + + If you are migrating from Swiftmailer (and the Swiftmailer bundle), be + warned that the DSN format is different. + +Using a 3rd Party Transport +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +But an easier option is to send emails via a 3rd party provider. Mailer supports +several - install whichever you want: + +================== ============================================= +Service Install with +================== ============================================= +Amazon SES ``composer require symfony/amazon-mailer`` +Gmail ``composer require symfony/google-mailer`` +MailChimp ``composer require symfony/mailchimp-mailer`` +Mailgun ``composer require symfony/mailgun-mailer`` +Postmark ``composer require symfony/postmark-mailer`` +SendGrid ``composer require symfony/sendgrid-mailer`` +================== ============================================= + +Each library includes a :ref:`Symfony Flex recipe ` that will add +example configuration to your ``.env`` file. For example, suppose you want to +use SendGrid. First, install it: + +.. code-block:: terminal + + $ composer require symfony/sendgrid-mailer + +You'll now have a new line in your ``.env`` file that you can uncomment: + +.. code-block:: bash + + # .env + MAILER_DSN=sendgrid://KEY@default + +The ``MAILER_DSN`` isn't a *real* address: it's a simple format that offloads +most of the configuration work to mailer. The ``sendgrid`` scheme activates the +SendGrid provider that you just installed, which knows all about how to deliver +messages to SendGrid. + +The *only* part you need to change is to replace ``KEY`` in the ``MAILER_DSN`` (in +``.env`` or ``.env.local``). + +Each provider has different environment variables that the Mailer uses to +configure the *actual* protocol, address and authentication for delivery. Some +also have options that can be configured with query parameters at the end of the +``MAILER_DSN`` - like ``?region=`` for Amazon SES. Some providers support +sending via ``http``, ``api`` or ``smtp``. Symfony chooses the best available +transport, but you can force to use one: + +.. code-block:: bash + + # .env + # force to use SMTP instead of HTTP (which is the default) + MAILER_DSN=sendgrid+smtp://$SENDGRID_KEY@default + +.. tip:: + + Check the :ref:`DSN formats ` for all supported providers. + +Creating & Sending Messages +--------------------------- + +To send an email, autowire the mailer using +:class:`Symfony\\Component\\Mailer\\MailerInterface` (service id ``mailer.mailer``) +and create an :class:`Symfony\\Component\\Mime\\Email` object:: + + // src/Controller/MailerController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Mailer\MailerInterface; + use Symfony\Component\Mime\Email; + + class MailerController extends AbstractController + { + /** + * @Route("/email") + */ + public function sendEmail(MailerInterface $mailer) + { + $email = (new Email()) + ->from('hello@example.com') + ->to('you@example.com') + //->cc('cc@example.com') + //->bcc('bcc@example.com') + //->replyTo('fabien@example.com') + //->priority(Email::PRIORITY_HIGH) + ->subject('Time for Symfony Mailer!') + ->text('Sending emails is fun again!') + ->html('

See Twig integration for better HTML integration!

'); + + /** @var Symfony\Component\Mailer\SentMessage $sentEmail */ + $sentEmail = $mailer->send($email); + // $messageId = $sentEmail->getMessageId(); + + // ... + } + } + +That's it! The message will be sent via whatever transport you configured. + +Email Addresses +~~~~~~~~~~~~~~~ + +All the methods that require email addresses (``from()``, ``to()``, etc.) accept +both strings or address objects:: + + // ... + use Symfony\Component\Mime\Address; + + $email = (new Email()) + // email address as a simple string + ->from('fabien@example.com') + + // email address as an object + ->from(new Address('fabien@example.com')) + + // defining the email address and name as an object + // (email clients will display the name) + ->from(new Address('fabien@example.com', 'Fabien')) + + // defining the email address and name as a string + // (the format must match: 'Name ') + ->from(Address::fromString('Fabien Potencier ')) + + // ... + ; + +.. tip:: + + Instead of calling ``->from()`` *every* time you create a new email, you can + create an :doc:`event subscriber ` and listen to the + :class:`Symfony\\Component\\Mailer\\Event\\MessageEvent` event to set the + same ``From`` email to all messages. + +Multiple addresses are defined with the ``addXXX()`` methods:: + + $email = (new Email()) + ->to('foo@example.com') + ->addTo('bar@example.com') + ->addTo('baz@example.com') + + // ... + ; + +Alternatively, you can pass multiple addresses to each method:: + + $toAddresses = ['foo@example.com', new Address('bar@example.com')]; + + $email = (new Email()) + ->to(...$toAddresses) + ->cc('cc1@example.com', 'cc2@example.com') + + // ... + ; + +Message Contents +~~~~~~~~~~~~~~~~ + +The text and HTML contents of the email messages can be strings (usually the +result of rendering some template) or PHP resources:: + + $email = (new Email()) + // ... + // simple contents defined as a string + ->text('Lorem ipsum...') + ->html('

Lorem ipsum...

') + + // attach a file stream + ->text(fopen('/path/to/emails/user_signup.txt', 'r')) + ->html(fopen('/path/to/emails/user_signup.html', 'r')) + ; + +.. tip:: + + You can also use Twig templates to render the HTML and text contents. Read + the `Twig: HTML & CSS`_ section later in this article to + learn more. + +File Attachments +~~~~~~~~~~~~~~~~ + +Use the ``attachFromPath()`` method to attach files that exist on your file system:: + + $email = (new Email()) + // ... + ->attachFromPath('/path/to/documents/terms-of-use.pdf') + // optionally you can tell email clients to display a custom name for the file + ->attachFromPath('/path/to/documents/privacy.pdf', 'Privacy Policy') + // optionally you can provide an explicit MIME type (otherwise it's guessed) + ->attachFromPath('/path/to/documents/contract.doc', 'Contract', 'application/msword') + // you can also use an absolute URL if your PHP config allows getting URLs using fopen() + // (this is not recommended because your application may or may not work depending on PHP config) + ->attachFromPath('http://example.com/path/to/documents/contract.doc', 'Contract', 'application/msword') + ; + +Alternatively you can use the ``attach()`` method to attach contents from a stream:: + + $email = (new Email()) + // ... + ->attach(fopen('/path/to/documents/contract.doc', 'r')) + ; + +Embedding Images +~~~~~~~~~~~~~~~~ + +If you want to display images inside your email, you must embed them +instead of adding them as attachments. When using Twig to render the email +contents, as explained :ref:`later in this article `, +the images are embedded automatically. Otherwise, you need to embed them manually. + +First, use the ``embed()`` or ``embedFromPath()`` method to add an image from a +file or stream:: + + $email = (new Email()) + // ... + // get the image contents from a PHP resource + ->embed(fopen('/path/to/images/logo.png', 'r'), 'logo') + // get the image contents from an existing file + ->embedFromPath('/path/to/images/signature.gif', 'footer-signature') + ; + +The second optional argument of both methods is the image name ("Content-ID" in +the MIME standard). Its value is an arbitrary string used later to reference the +images inside the HTML contents:: + + $email = (new Email()) + // ... + ->embed(fopen('/path/to/images/logo.png', 'r'), 'logo') + ->embedFromPath('/path/to/images/signature.gif', 'footer-signature') + // reference images using the syntax 'cid:' + "image embed name" + ->html(' ... ...') + ; + +Debugging Emails +---------------- + +The :class:`Symfony\\Component\\Mailer\\SentMessage` object returned by the +``send()`` method of the :class:`Symfony\\Component\\Mailer\\Transport\\TransportInterface` +provides access to the original message (``getOriginalMessage()``) and to some +debug information (``getDebug()``) such as the HTTP calls done by the HTTP +transports, which is useful to debug errors. + +.. note:: + + Some mailer providers change the ``Message-Id`` when sending the email. The + ``getMessageId()`` method from ``SentMessage`` always returns the definitive + ID of the message (being the original random ID generated by Symfony or the + new ID generated by the mailer provider). + +The exceptions related to mailer transports (those which implement +:class:`Symfony\\Component\\Mailer\\Exception\\TransportException`) also provide +this debug information via the ``getDebug()`` method. + +.. _mailer-twig: + +Twig: HTML & CSS +---------------- + +The Mime component integrates with the :ref:`Twig template engine ` +to provide advanced features such as CSS style inlining and support for HTML/CSS +frameworks to create complex HTML email messages. First, make sure Twig is installed: + +.. code-block:: terminal + + $ composer require symfony/twig-bundle + +HTML Content +~~~~~~~~~~~~ + +To define the contents of your email with Twig, use the +:class:`Symfony\\Bridge\\Twig\\Mime\\TemplatedEmail` class. This class extends +the normal :class:`Symfony\\Component\\Mime\\Email` class but adds some new methods +for Twig templates:: + + use Symfony\Bridge\Twig\Mime\TemplatedEmail; + + $email = (new TemplatedEmail()) + ->from('fabien@example.com') + ->to(new Address('ryan@example.com')) + ->subject('Thanks for signing up!') + + // path of the Twig template to render + ->htmlTemplate('emails/signup.html.twig') + + // pass variables (name => value) to the template + ->context([ + 'expiration_date' => new \DateTime('+7 days'), + 'username' => 'foo', + ]) + ; + +Then, create the template: + +.. code-block:: html+twig + + {# templates/emails/signup.html.twig #} +

Welcome {{ email.toName }}!

+ +

+ You signed up as {{ username }} the following email: +

+

{{ email.to[0].address }}

+ +

+ Click here to activate your account + (this link is valid until {{ expiration_date|date('F jS') }}) +

+ +The Twig template has access to any of the parameters passed in the ``context()`` +method of the ``TemplatedEmail`` class and also to a special variable called +``email``, which is an instance of +:class:`Symfony\\Bridge\\Twig\\Mime\\WrappedTemplatedEmail`. + +Text Content +~~~~~~~~~~~~ + +When the text content of a ``TemplatedEmail`` is not explicitly defined, mailer +will generate it automatically by converting the HTML contents into text. If you +have `league/html-to-markdown`_ installed in your application, +it uses that to turn HTML into Markdown (so the text email has some visual appeal). +Otherwise, it applies the :phpfunction:`strip_tags` PHP function to the original +HTML contents. + +If you want to define the text content yourself, use the ``text()`` method +explained in the previous sections or the ``textTemplate()`` method provided by +the ``TemplatedEmail`` class: + +.. code-block:: diff + + + use Symfony\Bridge\Twig\Mime\TemplatedEmail; + + $email = (new TemplatedEmail()) + // ... + + ->htmlTemplate('emails/signup.html.twig') + + ->textTemplate('emails/signup.txt.twig') + // ... + ; + +.. _mailer-twig-embedding-images: + +Embedding Images +~~~~~~~~~~~~~~~~ + +Instead of dealing with the ```` syntax explained in the +previous sections, when using Twig to render email contents you can refer to +image files as usual. First, to simplify things, define a Twig namespace called +``images`` that points to whatever directory your images are stored in: + +.. code-block:: yaml + + # config/packages/twig.yaml + twig: + # ... + + paths: + # point this wherever your images live + '%kernel.project_dir%/assets/images': images + +Now, use the special ``email.image()`` Twig helper to embed the images inside +the email contents: + +.. code-block:: html+twig + + {# '@images/' refers to the Twig namespace defined earlier #} + Logo + +

Welcome {{ email.toName }}!

+ {# ... #} + +.. _mailer-inline-css: + +Inlining CSS Styles +~~~~~~~~~~~~~~~~~~~ + +Designing the HTML contents of an email is very different from designing a +normal HTML page. For starters, most email clients only support a subset of all +CSS features. In addition, popular email clients like Gmail don't support +defining styles inside ```` sections and you must **inline +all the CSS styles**. + +CSS inlining means that every HTML tag must define a ``style`` attribute with +all its CSS styles. This can make organizing your CSS a mess. That's why Twig +provides a ``CssInlinerExtension`` that automates everything for you. Install +it with: + +.. code-block:: terminal + + $ composer require twig/extra-bundle twig/cssinliner-extra + +The extension is enabled automatically. To use it, wrap the entire template +with the ``inline_css`` filter: + +.. code-block:: html+twig + + {% apply inline_css %} + + +

Welcome {{ email.toName }}!

+ {# ... #} + {% endapply %} + +Using External CSS Files +........................ + +You can also define CSS styles in external files and pass them as +arguments to the filter: + +.. code-block:: html+twig + + {% apply inline_css(source('@css/email.css')) %} +

Welcome {{ username }}!

+ {# ... #} + {% endapply %} + +You can pass unlimited number of arguments to ``inline_css()`` to load multiple +CSS files. For this example to work, you also need to define a new Twig namespace +called ``css`` that points to the directory where ``email.css`` lives: + +.. _mailer-css-namespace: + +.. code-block:: yaml + + # config/packages/twig.yaml + twig: + # ... + + paths: + # point this wherever your css files live + '%kernel.project_dir%/assets/css': css + +.. _mailer-markdown: + +Rendering Markdown Content +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Twig provides another extension called ``MarkdownExtension`` that lets you +define the email contents using `Markdown syntax`_. To use this, install the +extension and a Markdown conversion library (the extension is compatible with +several popular libraries): + +.. code-block:: terminal + + # instead of league/commonmark, you can also use erusev/parsedown or michelf/php-markdown + $ composer require twig/markdown-extension league/commonmark + +The extension adds a ``markdown`` filter, which you can use to convert parts or +the entire email contents from Markdown to HTML: + +.. code-block:: twig + + {% apply markdown %} + Welcome {{ email.toName }}! + =========================== + + You signed up to our site using the following email: + `{{ email.to[0].address }}` + + [Click here to activate your account]({{ url('...') }}) + {% endapply %} + +.. _mailer-inky: + +Inky Email Templating Language +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Creating beautifully designed emails that work on every email client is so +complex that there are HTML/CSS frameworks dedicated to that. One of the most +popular frameworks is called `Inky`_. It defines a syntax based on some simple +tags which are later transformed into the real HTML code sent to users: + +.. code-block:: html + + + + + This is a column. + + + +Twig provides integration with Inky via the ``InkyExtension``. First, install +the extension in your application: + +.. code-block:: terminal + + $ composer require twig/extra-bundle twig/inky-extra + +The extension adds an ``inky_to_html`` filter, which can be used to convert +parts or the entire email contents from Inky to HTML: + +.. code-block:: html+twig + + {% apply inky_to_html %} + + + + +

Welcome {{ email.toName }}!

+
+ + {# ... #} +
+
+ {% endapply %} + +You can combine all filters to create complex email messages: + +.. code-block:: twig + + {% apply inky_to_html|inline_css(source('@css/foundation-emails.css')) %} + {# ... #} + {% endapply %} + +This makes use of the :ref:`css Twig namespace ` we created +earlier. You could, for example, `download the foundation-emails.css file`_ +directly from GitHub and save it in ``assets/css``. + +Signing and Encrypting Messages +------------------------------- + +It's possible to sign and/or encrypt email messages applying the `S/MIME`_ +standard to increase their integrity/security. Both options can be combined to +encrypt a signed message and/or to sign an encrypted message. + +Before signing/encrypting messages, make sure to have: + +* The `OpenSSL PHP extension`_ properly installed and configured; +* A valid S/MIME security certificate. + +Signing Messages +~~~~~~~~~~~~~~~~ + +When signing a message, a cryptographic hash is generated for the entire content +of the message (including attachments). This hash is added as an attachment so +the recipient can validate the integrity of the received message. However, the +contents of the original message are still readable for mailing agents not +supporting signed messages, so you must also encrypt the message if you want to +hide its contents:: + + use Symfony\Component\Mime\Crypto\SMimeSigner; + use Symfony\Component\Mime\Email; + + $email = (new Email()) + ->from('hello@example.com') + // ... + ->html('...'); + + $signer = new SMimeSigner('/path/to/certificate.crt', '/path/to/certificate-private-key.key'); + // if the private key has a passphrase, pass it as the third argument + // new SMimeSigner('/path/to/certificate.crt', '/path/to/certificate-private-key.key', 'the-passphrase'); + + $signedEmail = $signer->sign($email); + // now use the Mailer component to send this $signedEmail instead of the original email + +The certificate and private key must be `PEM encoded`_, and can be either +created using for example OpenSSL or obtained at an official Certificate +Authority (CA). The email recipient must have the CA certificate in the list of +trusted issuers in order to verify the signature. + +.. tip:: + + When using OpenSSL to generate certificates, make sure to add the + ``-addtrust emailProtection`` command option. + +.. tip:: + + The ``SMimeSigner`` class defines other optional arguments to pass + intermediate certificates and to configure the signing process using a + bitwise operator options for :phpfunction:`openssl_pkcs7_sign` PHP function. + +Encrypting Messages +~~~~~~~~~~~~~~~~~~~ + +When encrypting a message, the entire message (including attachments) is +encrypted using a certificate. Therefore, only the recipients that have the +corresponding private key can read the original message contents:: + + use Symfony\Component\Mime\Crypto\SMimeEncrypter; + use Symfony\Component\Mime\Email; + + $email = (new Email()) + ->from('hello@example.com') + // ... + ->html('...'); + + $encrypter = new SMimeEncrypter('/path/to/certificate.crt'); + $encryptedEmail = $encrypter->encrypt($email); + // now use the Mailer component to send this $encryptedEmail instead of the original email + +You can pass more than one certificate to the ``SMimeEncrypter()`` constructor +and it will select the appropriate certificate depending on the ``To`` option:: + + $firstEmail = (new Email()) + // ... + ->to('jane@example.com'); + + $secondEmail = (new Email()) + // ... + ->to('john@example.com'); + + // the second optional argument of SMimeEncrypter defines which encryption algorithm is used + // (it must be one of these constants: https://www.php.net/manual/en/openssl.ciphers.php) + $encrypter = new SMimeEncrypter([ + // key = email recipient; value = path to the certificate file + 'jane@example.com' => '/path/to/first-certificate.crt', + 'john@example.com' => '/path/to/second-certificate.crt', + ]); + + $firstEncryptedEmail = $encrypter->encrypt($firstEmail); + $secondEncryptedEmail = $encrypter->encrypt($secondEmail); + +Sending Messages Async +---------------------- + +When you call ``$mailer->send($email)``, the email is sent to the transport immediately. +To improve performance, you can leverage :doc:`Messenger ` to send +the messages later via a Messenger transport. + +Start by following the :doc:`Messenger ` documentation and configuring +a transport. Once everything is set up, when you call ``$mailer->send()``, a +:class:`Symfony\\Component\\Mailer\\Messenger\\SendEmailMessage` message will +be dispatched through the default message bus (``messenger.default_bus``). Assuming +you have a transport called ``async``, you can route the message there: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + async: "%env(MESSENGER_TRANSPORT_DSN)%" + + routing: + 'Symfony\Component\Mailer\Messenger\SendEmailMessage': async + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + $container->loadFromExtension('framework', [ + 'messenger' => [ + 'routing' => [ + 'Symfony\Component\Mailer\Messenger\SendEmailMessage' => 'async', + ], + ], + ]); + +Thanks to this, instead of being delivered immediately, messages will be sent to +the transport to be handled later (see :ref:`messenger-worker`). + +Multiple Email Transports +------------------------- + +You may want to use more than one mailer transport for delivery of your messages. +This can be configured by replacing the ``dsn`` configuration entry with a +``transports`` entry, like: + +.. code-block:: yaml + + # config/packages/mailer.yaml + framework: + mailer: + transports: + main: '%env(MAILER_DSN)%' + important: '%env(MAILER_DSN_IMPORTANT)%' + +By default the first transport is used. The other transports can be used by +adding a text header ``X-Transport`` to an email:: + + // Send using first "main" transport ... + $mailer->send($email); + + // ... or use the "important" one + $email->getHeaders()->addTextHeader('X-Transport', 'important'); + $mailer->send($email); + +Development & Debugging +----------------------- + +Disabling Delivery +~~~~~~~~~~~~~~~~~~ + +While developing (or testing), you may want to disable delivery of messages entirely. +You can do this by forcing Mailer to use the ``NullTransport`` in only the ``dev`` +environment: + +.. code-block:: yaml + + # config/packages/dev/mailer.yaml + framework: + mailer: + dsn: 'null://null' + +.. note:: + + If you're using Messenger and routing to a transport, the message will *still* + be sent to that transport. + +Always Send to the Same Address +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Instead of disabling delivery entirely, you might want to *always* send emails to +a specific address, instead of the *real* address. To do that, you can take +advantage of the ``EnvelopeListener`` and register it *only* for the ``dev`` +environment: + +.. code-block:: yaml + + # config/services_dev.yaml + services: + mailer.dev.set_recipients: + class: Symfony\Component\Mailer\EventListener\EnvelopeListener + tags: ['kernel.event_subscriber'] + arguments: + $sender: null + $recipients: ['youremail@example.com'] + +.. _`download the foundation-emails.css file`: https://github.com/zurb/foundation-emails/blob/develop/dist/foundation-emails.css +.. _`league/html-to-markdown`: https://github.com/thephpleague/html-to-markdown +.. _`Markdown syntax`: https://commonmark.org/ +.. _`Inky`: https://foundation.zurb.com/emails.html +.. _`S/MIME`: https://en.wikipedia.org/wiki/S/MIME +.. _`OpenSSL PHP extension`: https://php.net/manual/en/book.openssl.php +.. _`PEM encoded`: https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail diff --git a/mercure.rst b/mercure.rst new file mode 100644 index 00000000000..b9a738bc13b --- /dev/null +++ b/mercure.rst @@ -0,0 +1,624 @@ +.. index:: + single: Mercure + +Pushing Data to Clients Using the Mercure Protocol +================================================== + +Being able to broadcast data in real-time from servers to clients is a +requirement for many modern web and mobile applications. + +Creating a UI reacting in live to changes made by other users +(e.g. a user changes the data currently browsed by several other users, +all UIs are instantly updated), +notifying the user when :doc:`an asynchronous job ` has been +completed or creating chat applications are among the typical use cases +requiring "push" capabilities. + +Symfony provides a straightforward component, built on top of +`the Mercure protocol`_, specifically designed for this class of use cases. + +Mercure is an open protocol designed from the ground to publish updates from +server to clients. It is a modern and efficient alternative to timer-based +polling and to WebSocket. + +Because it is built on top `Server-Sent Events (SSE)`_, Mercure is supported +out of the box in most modern browsers (Edge and IE require `a polyfill`_) and +has `high-level implementations`_ in many programming languages. + +Mercure comes with an authorization mechanism, +automatic re-connection in case of network issues +with retrieving of lost updates, "connection-less" push for smartphones and +auto-discoverability (a supported client can automatically discover and +subscribe to updates of a given resource thanks to a specific HTTP header). + +All these features are supported in the Symfony integration. + +Unlike WebSocket, which is only compatible with HTTP 1.x, +Mercure leverages the multiplexing capabilities provided by HTTP/2 +and HTTP/3 (but also supports older versions of HTTP). + +`In this recording`_ you can see how a Symfony web API leverages Mercure +and API Platform to update in live a React app and a mobile app (React Native) +generated using the API Platform client generator. + +Installation +------------ + +Installing the Symfony Component +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In applications using :ref:`Symfony Flex `, run this command to +install the Mercure support before using it: + +.. code-block:: terminal + + $ composer require mercure + +Running a Mercure Hub +~~~~~~~~~~~~~~~~~~~~~ + +To manage persistent connections, Mercure relies on a Hub: a dedicated server +that handles persistent SSE connections with the clients. +The Symfony app publishes the updates to the hub, that will broadcast them to +clients. + +.. image:: /_images/mercure/schema.png + +An official and open source (AGPL) implementation of a Hub can be downloaded +as a static binary from `Mercure.rocks`_. + +Run the following command to start it: + +.. code-block:: terminal + + $ ./mercure --jwt-key='aVerySecretKey' --addr='localhost:3000' --allow-anonymous --cors-allowed-origins='*' + +.. note:: + + Alternatively to the binary, a Docker image, a Helm chart for Kubernetes + and a managed, High Availability Hub are also provided by Mercure.rocks. + +.. tip:: + + The `API Platform distribution`_ comes with a Docker Compose configuration + as well as a Helm chart for Kubernetes that are 100% compatible with Symfony, + and contain a Mercure hub. + You can copy them in your project, even if you don't use API Platform. + +Configuration +------------- + +The preferred way to configure the MercureBundle is using +:doc:`environment variables `. + +Set the URL of your hub as the value of the ``MERCURE_PUBLISH_URL`` env var. +The ``.env`` file of your project has been updated by the Flex recipe to +provide example values. +Set it to the URL of the Mercure Hub (``http://localhost:3000/.well-known/mercure`` by default). + +In addition, the Symfony application must bear a `JSON Web Token`_ (JWT) +to the Mercure Hub to be authorized to publish updates. + +This JWT should be stored in the ``MERCURE_JWT_TOKEN`` environment variable. + +The JWT must be signed with the same secret key as the one used by +the Hub to verify the JWT (``aVerySecretKey`` in our example). +Its payload must contain at least the following structure to be allowed to +publish: + +.. code-block:: json + + { + "mercure": { + "publish": [] + } + } + +Because the array is empty, the Symfony app will only be authorized to publish +public updates (see the authorization_ section for further information). + +.. tip:: + + The jwt.io website is a convenient way to create and sign JWTs. + Checkout this `example JWT`_, that grants publishing rights for all *targets* + (notice the star in the array). + Don't forget to set your secret key properly in the bottom of the right panel of the form! + +.. caution:: + + Don't put the secret key in ``MERCURE_JWT_TOKEN``, it will not work! + This environment variable must contain a JWT, signed with the secret key. + + Also, be sure to keep both the secret key and the JWTs... secrets! + +Basic Usage +----------- + +Publishing +~~~~~~~~~~ + +The Mercure Component provides an ``Update`` value object representing +the update to publish. It also provides a ``Publisher`` service to dispatch +updates to the Hub. + +The ``Publisher`` service can be injected using the +:doc:`autowiring ` in any other +service, including controllers:: + + // src/Controller/PublishController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Mercure\Publisher; + use Symfony\Component\Mercure\Update; + + class PublishController + { + public function __invoke(Publisher $publisher): Response + { + $update = new Update( + 'http://example.com/books/1', + json_encode(['status' => 'OutOfStock']) + ); + + // The Publisher service is an invokable object + $publisher($update); + + return new Response('published!'); + } + } + +The first parameter to pass to the ``Update`` constructor is +the **topic** being updated. This topic should be an `IRI`_ +(Internationalized Resource Identifier, RFC 3987): a unique identifier +of the resource being dispatched. + +Usually, this parameter contains the original URL of the resource +transmitted to the client, but it can be any valid `IRI`_, it doesn't +have to be a URL that exists (similarly to XML namespaces). + +The second parameter of the constructor is the content of the update. +It can be anything, stored in any format. +However, serializing the resource in a hypermedia format such as JSON-LD, +Atom, HTML or XML is recommended. + +Subscribing +~~~~~~~~~~~ + +Subscribing to updates in JavaScript is straightforward: + +.. code-block:: javascript + + const eventSource = new EventSource('http://localhost:3000/.well-known/mercure?topic=' + encodeURIComponent('http://example.com/books/1')); + eventSource.onmessage = event => { + // Will be called every time an update is published by the server + console.log(JSON.parse(event.data)); + } + +Mercure also allows to subscribe to several topics, +and to use URI Templates as patterns: + +.. code-block:: javascript + + // URL is a built-in JavaScript class to manipulate URLs + const url = new URL('http://localhost:3000/.well-known/mercure'); + url.searchParams.append('topic', 'http://example.com/books/1'); + // Subscribe to updates of several Book resources + url.searchParams.append('topic', 'http://example.com/books/2'); + // All Review resources will match this pattern + url.searchParams.append('topic', 'http://example.com/reviews/{id}'); + + const eventSource = new EventSource(url); + eventSource.onmessage = event => { + console.log(JSON.parse(event.data)); + } + +.. tip:: + + Google Chrome DevTools natively integrate a `practical UI`_ displaying in live + the received events: + + .. image:: /_images/mercure/chrome.png + + To use it: + + * open the DevTools + * select the "Network" tab + * click on the request to the Mercure hub + * click on the "EventStream" sub-tab. + +.. tip:: + + Test if a URI Template match a URL using `the online debugger`_ + +Async dispatching +----------------- + +Instead of calling the ``Publisher`` service directly, you can also let Symfony +dispatching the updates asynchronously thanks to the provided integration with +the Messenger component. + +First, be sure :doc:`to install the Messenger component ` +and to configure properly a transport (if you don't, the handler will +be called synchronously). + +Then, dispatch the Mercure ``Update`` to the Messenger's Message Bus, +it will be handled automatically:: + + // src/Controller/PublishController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Mercure\Update; + use Symfony\Component\Messenger\MessageBusInterface; + + class PublishController + { + public function __invoke(MessageBusInterface $bus): Response + { + $update = new Update( + 'http://example.com/books/1', + json_encode(['status' => 'OutOfStock']) + ); + + // Sync, or async (RabbitMQ, Kafka...) + $bus->dispatch($update); + + return new Response('published!'); + } + } + +Discovery +--------- + +The Mercure protocol comes with a discovery mechanism. +To leverage it, the Symfony application must expose the URL of the Mercure Hub +in a ``Link`` HTTP header. + +.. image:: /_images/mercure/discovery.png + +You can create ``Link`` headers with the :doc:`WebLink Component `, +by using the ``AbstractController::addLink`` helper method:: + + // src/Controller/DiscoverController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\JsonResponse; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\WebLink\LInk; + + class DiscoverController extends AbstractController + { + public function __invoke(Request $request): JsonResponse + { + // This parameter is automatically created by the MercureBundle + $hubUrl = $this->getParameter('mercure.default_hub'); + + // Link: ; rel="mercure" + $this->addLink($request, new Link('mercure', $hubUrl)); + + return $this->json([ + '@id' => '/books/1', + 'availability' => 'https://schema.org/InStock', + ]); + } + } + +Then, this header can be parsed client-side to find the URL of the Hub, +and to subscribe to it: + +.. code-block:: javascript + + // Fetch the original resource served by the Symfony web API + fetch('/books/1') // Has Link: ; rel="mercure" + .then(response => { + // Extract the hub URL from the Link header + const hubUrl = response.headers.get('Link').match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1]; + + // Append the topic(s) to subscribe as query parameter + const hub = new URL(hubUrl); + hub.searchParams.append('topic', 'http://example.com/books/{id}'); + + // Subscribe to updates + const eventSource = new EventSource(hub); + eventSource.onmessage = event => console.log(event.data); + }); + +Authorization +------------- + +Mercure also allows to dispatch updates only to authorized clients. +To do so, set the list of **targets** allowed to receive the update +as the third parameter of the ``Update`` constructor:: + + // src/Controller/Publish.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Mercure\Publisher; + use Symfony\Component\Mercure\Update; + + class PublishController + { + public function __invoke(Publisher $publisher): Response + { + $update = new Update( + 'http://example.com/books/1', + json_encode(['status' => 'OutOfStock']), + ['http://example.com/user/kevin', 'http://example.com/groups/admin'] // Here are the targets + ); + + // Publisher's JWT must contain all of these targets or * in mercure.publish or you'll get a 401 + // Subscriber's JWT must contain at least one of these targets or * in mercure.subscribe to receive the update + $publisher($update); + + return new Response('published to the selected targets!'); + } + } + +To subscribe to private updates, subscribers must provide +a JWT containing at least one target marking the update to the Hub. + +To provide this JWT, the subscriber can use a cookie, +or a ``Authorization`` HTTP header. +Cookies are automatically sent by the browsers when opening an ``EventSource`` connection. +Using cookies is the most secure and preferred way when the client is a web browser. +If the client is not a web browser, then using an authorization header is the way to go. + +.. tip:: + + The native implementation of EventSource doesn't allow specifying headers. + For example, authorization using Bearer token. In order to achieve that, use `a polyfill`_ + + .. code-block:: javascript + + const es = new EventSourcePolyfill(url, { + headers: { + 'Authorization': 'Bearer ' + token, + } + }); + +In the following example controller, +the generated cookie contains a JWT, itself containing the appropriate targets. +This cookie will be automatically sent by the web browser when connecting to the Hub. +Then, the Hub will verify the validity of the provided JWT, and extract the targets +from it. + +To generate the JWT, we'll use the ``lcobucci/jwt`` library. Install it: + +.. code-block:: terminal + + $ composer require lcobucci/jwt + +And here is the controller:: + + // src/Controller/DiscoverController.php + namespace App\Controller; + + use Lcobucci\JWT\Builder; + use Lcobucci\JWT\Signer\Hmac\Sha256; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\WebLink\Link; + + class DiscoverController extends AbstractController + { + public function __invoke(Request $request): Response + { + $hubUrl = $this->getParameter('mercure.default_hub'); + $this->addLink($request, new Link('mercure', $hubUrl)); + + $username = $this->getUser()->getUsername(); // Retrieve the username of the current user + $token = (new Builder()) + // set other appropriate JWT claims, such as an expiration date + ->set('mercure', ['subscribe' => ["http://example.com/user/$username"]]) // could also include the security roles, or anything else + ->sign(new Sha256(), $this->getParameter('mercure_secret_key')) // don't forget to set this parameter! Test value: aVerySecretKey + ->getToken(); + + $response = $this->json(['@id' => '/demo/books/1', 'availability' => 'https://schema.org/InStock']); + $response->headers->set( + 'set-cookie', + sprintf('mercureAuthorization=%s; path=/.well-known/mercure; secure; httponly; SameSite=strict', $token) + ); + + return $response; + } + } + +.. caution:: + + To use the cookie authentication method, the Symfony app and the Hub + must be served from the same domain (can be different sub-domains). + +Generating Programmatically The JWT Used to Publish +--------------------------------------------------- + +Instead of directly storing a JWT in the configuration, +you can create a service that will return the token used by +the ``Publisher`` object:: + + // src/Mercure/MyJwtProvider.php + namespace App\Mercure; + + final class MyJwtProvider + { + public function __invoke(): string + { + return 'the-JWT'; + } + } + +Then, reference this service in the bundle configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mercure.yaml + mercure: + hubs: + default: + url: https://mercure-hub.example.com/.well-known/mercure + jwt_provider: App\Mercure\MyJwtProvider + + .. code-block:: xml + + + + + + + + .. code-block:: php + + // config/packages/mercure.php + use App\Mercure\MyJwtProvider; + + $container->loadFromExtension('mercure', [ + 'hubs' => [ + 'default' => [ + 'url' => 'https://mercure-hub.example.com/.well-known/mercure', + 'jwt_provider' => MyJwtProvider::class, + ], + ], + ]); + +This method is especially convenient when using tokens having an expiration +date, that can be refreshed programmatically. + +Web APIs +-------- + +When creating a web API, it's convenient to be able to instantly push +new versions of the resources to all connected devices, and to update +their views. + +API Platform can use the Mercure Component to dispatch updates automatically, +every time an API resource is created, modified or deleted. + +Start by installing the library using its official recipe: + +.. code-block:: terminal + + $ composer require api + +Then, creating the following entity is enough to get a fully-featured +hypermedia API, and automatic update broadcasting through the Mercure hub:: + + // src/Entity/Book.php + namespace App\Entity; + + use ApiPlatform\Core\Annotation\ApiResource; + use Doctrine\ORM\Mapping as ORM; + + /** + * @ApiResource(mercure=true) + * @ORM\Entity + */ + class Book + { + /** + * @ORM\Id + * @ORM\Column + */ + public $name; + + /** + * @ORM\Column + */ + public $status; + } + +As showcased `in this recording`_, the API Platform Client Generator also +allows to scaffold complete React and React Native applications from this API. +These applications will render the content of Mercure updates in real-time. + +Checkout `the dedicated API Platform documentation`_ to learn more about +its Mercure support. + +Testing +-------- + +During functional testing there is no need to send updates to Mercure. They will +be handled by a stub publisher:: + + // tests/Functional/Fixtures/PublisherStub.php + namespace App\Tests\Functional\Fixtures; + + use Symfony\Component\Mercure\Update; + + class PublisherStub + { + public function __invoke(Update $update): string + { + return ''; + } + } + +PublisherStub decorates the default publisher service so no updates are actually +sent. Here is the PublisherStub implementation:: + + # config/services_test.yaml + App\Tests\Functional\Fixtures\PublisherStub: + decorates: mercure.hub.default.publisher + + +Debugging +--------- + +.. versionadded:: 0.2 + + The WebProfiler panel was introduced in MercureBundle 0.2. + +Enable the panel in your configuration, as follows: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mercure.yaml + mercure: + enable_profiler: '%kernel.debug%' + + .. code-block:: xml + + + + + + + + + + .. code-block:: php + + // config/packages/mercure.php + $container->loadFromExtension('mercure', [ + 'enable_profiler' => '%kernel.debug%', + ]); + + +.. image:: /_images/mercure/panel.png + +.. _`the Mercure protocol`: https://github.com/dunglas/mercure#protocol-specification +.. _`Server-Sent Events (SSE)`: https://developer.mozilla.org/docs/Server-sent_events +.. _`a polyfill`: https://github.com/Yaffle/EventSource +.. _`high-level implementations`: https://github.com/dunglas/mercure#tools +.. _`In this recording`: https://www.youtube.com/watch?v=UI1l0JOjLeI +.. _`Mercure.rocks`: https://mercure.rocks +.. _`API Platform distribution`: https://api-platform.com/docs/distribution/ +.. _`JSON Web Token`: https://tools.ietf.org/html/rfc7519 +.. _`example JWT`: https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.iHLdpAEjX4BqCsHJEegxRmO-Y6sMxXwNATrQyRNt3GY +.. _`IRI`: https://tools.ietf.org/html/rfc3987 +.. _`practical UI`: https://twitter.com/chromedevtools/status/562324683194785792 +.. _`the dedicated API Platform documentation`: https://api-platform.com/docs/core/mercure/ +.. _`the online debugger`: https://uri-template-tester.mercure.rocks diff --git a/messenger.rst b/messenger.rst new file mode 100644 index 00000000000..7287dff3ac7 --- /dev/null +++ b/messenger.rst @@ -0,0 +1,1487 @@ +.. index:: + single: Messenger + +Messenger: Sync & Queued Message Handling +========================================= + +Messenger provides a message bus with the ability to send messages and then +handle them immediately in your application or send them through transports +(e.g. queues) to be handled later. To learn more deeply about it, read the +:doc:`Messenger component docs `. + +Installation +------------ + +In applications using :ref:`Symfony Flex `, run this command to +install messenger: + +.. code-block:: terminal + + $ composer require messenger + +Creating a Message & Handler +---------------------------- + +Messenger centers around two different classes that you'll create: (1) a message +class that holds data and (2) a handler(s) class that will be called when that +message is dispatched. The handler class will read the message class and perform +some task. + +There are no specific requirements for a message class, except that it can be +serialized:: + + // src/Message/SmsNotification.php + namespace App\Message; + + class SmsNotification + { + private $content; + + public function __construct(string $content) + { + $this->content = $content; + } + + public function getContent(): string + { + return $this->content; + } + } + +.. _messenger-handler: + +A message handler is a PHP callable, the recommended way to create it is to +create a class that implements :class:`Symfony\\Component\\Messenger\\Handler\\MessageHandlerInterface` +and has an ``__invoke()`` method that's type-hinted with the message class (or a +message interface):: + + // src/MessageHandler/SmsNotificationHandler.php + namespace App\MessageHandler; + + use App\Message\SmsNotification; + use Symfony\Component\Messenger\Handler\MessageHandlerInterface; + + class SmsNotificationHandler implements MessageHandlerInterface + { + public function __invoke(SmsNotification $message) + { + // ... do some work - like sending an SMS message! + } + } + +Thanks to :ref:`autoconfiguration ` and the ``SmsNotification`` +type-hint, Symfony knows that this handler should be called when an ``SmsNotification`` +message is dispatched. Most of the time, this is all you need to do. But you can +also :ref:`manually configure message handlers `. To +see all the configured handlers, run: + +.. code-block:: terminal + + $ php bin/console debug:messenger + +Dispatching the Message +----------------------- + +You're ready! To dispatch the message (and call the handler), inject the +``message_bus`` service (via the ``MessageBusInterface``), like in a controller:: + + // src/Controller/DefaultController.php + namespace App\Controller; + + use App\Message\SmsNotification; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Messenger\MessageBusInterface; + + class DefaultController extends AbstractController + { + public function index(MessageBusInterface $bus) + { + // will cause the SmsNotificationHandler to be called + $bus->dispatch(new SmsNotification('Look! I created a message!')); + + // or use the shortcut + $this->dispatchMessage(new SmsNotification('Look! I created a message!')); + + // ... + } + } + +Transports: Async/Queued Messages +--------------------------------- + +By default, messages are handled as soon as they are dispatched. If you want +to handle a message asynchronously, you can configure a transport. A transport +is capable of sending messages (e.g. to a queueing system) and then +:ref:`receiving them via a worker `. Messenger supports +:ref:`multiple transports `. + +.. note:: + + If you want to use a transport that's not supported, check out the + `Enqueue's transport`_, which supports things like Kafka, Amazon SQS and + Google Pub/Sub. + +A transport is registered using a "DSN". Thanks to Messenger's Flex recipe, your +``.env`` file already has a few examples. + +.. code-block:: bash + + # MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages + # MESSENGER_TRANSPORT_DSN=doctrine://default + # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages + +Uncomment whichever transport you want (or set it in ``.env.local``). See +:ref:`messenger-transports-config` for more details. + +Next, in ``config/packages/messenger.yaml``, let's define a transport called ``async`` +that uses this configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + async: "%env(MESSENGER_TRANSPORT_DSN)%" + + # or expanded to configure more options + #async: + # dsn: "%env(MESSENGER_TRANSPORT_DSN)%" + # options: [] + + .. code-block:: xml + + + + + + + + %env(MESSENGER_TRANSPORT_DSN)% + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + $container->loadFromExtension('framework', [ + 'messenger' => [ + 'transports' => [ + 'async' => '%env(MESSENGER_TRANSPORT_DSN)%', + + // or expanded to configure more options + 'async' => [ + 'dsn' => '%env(MESSENGER_TRANSPORT_DSN)%', + 'options' => [] + ], + ], + ], + ]); + +.. _messenger-routing: + +Routing Messages to a Transport +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now that you have a transport configured, instead of handling a message immediately, +you can configure them to be sent to a transport: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + async: "%env(MESSENGER_TRANSPORT_DSN)%" + + routing: + # async is whatever name you gave your transport above + 'App\Message\SmsNotification': async + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + $container->loadFromExtension('framework', [ + 'messenger' => [ + 'routing' => [ + // async is whatever name you gave your transport above + 'App\Message\SmsNotification' => 'async', + ], + ], + ]); + +Thanks to this, the ``App\Message\SmsNotification`` will be sent to the ``async`` +transport and its handler(s) will *not* be called immediately. Any messages not +matched under ``routing`` will still be handled immediately. + +You can also route classes by their parent class or interface. Or send messages +to multiple transport: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + routing: + # route all messages that extend this example base class or interface + 'App\Message\AbstractAsyncMessage': async + 'App\Message\AsyncMessageInterface': async + + 'My\Message\ToBeSentToTwoSenders': [async, audit] + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + $container->loadFromExtension('framework', [ + 'messenger' => [ + 'routing' => [ + // route all messages that extend this example base class or interface + 'App\Message\AbstractAsyncMessage' => 'async', + 'App\Message\AsyncMessageInterface' => 'async', + 'My\Message\ToBeSentToTwoSenders' => ['async', 'audit'], + ], + ], + ]); + +Doctrine Entities in Messages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to pass a Doctrine entity in a message, it's better to pass the entity's +primary key (or whatever relevant information the handler actually needs, like ``email``, +etc) instead of the object:: + + class NewUserWelcomeEmail + { + private $userId; + + public function __construct(int $userId) + { + $this->userId = $userId; + } + + public function getUserId(): int + { + return $this->userId; + } + } + +Then, in your handler, you can query for a fresh object:: + + // src/MessageHandler/NewUserWelcomeEmailHandler.php + namespace App\MessageHandler; + + use App\Message\NewUserWelcomeEmail; + use App\Repository\UserRepository; + use Symfony\Component\Messenger\Handler\MessageHandlerInterface; + + class NewUserWelcomeEmailHandler implements MessageHandlerInterface + { + private $userRepository; + + public function __construct(UserRepository $userRepository) + { + $this->userRepository = $userRepository; + } + + public function __invoke(NewUserWelcomeEmail $welcomeEmail) + { + $user = $this->userRepository->find($welcomeEmail->getUserId()); + + // ... send an email! + } + } + +This guarantees the entity contains fresh data. + +Handling Messages Synchronously +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a message doesn't :ref:`match any routing rules `, it won't +be sent to any transport and will be handled immediately. In some cases (like +when `binding handlers to different transports`_), +it's easier or more flexible to handle this explicitly: by creating a ``sync`` +transport and "sending" messages there to be handled immediately: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + # ... other transports + + sync: 'sync://' + + routing: + App\Message\SmsNotification: sync + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + $container->loadFromExtension('framework', [ + 'messenger' => [ + 'transports' => [ + // ... other transports + + 'sync' => 'sync://', + ], + 'routing' => [ + 'App\Message\SmsNotification' => 'sync', + ], + ], + ]); + +Creating your Own Transport +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can also create your own transport if you need to send or receive messages +from something that is not supported. See :doc:`/messenger/custom-transport`. + +.. _messenger-worker: + +Consuming Messages (Running the Worker) +--------------------------------------- + +Once your messages have been routed, in most cases, you'll need to "consume" them. +You can do this with the ``messenger:consume`` command: + +.. code-block:: terminal + + $ php bin/console messenger:consume async + + # use -vv to see details about what's happening + $ php bin/console messenger:consume async -vv + +The first argument is the receiver's name (or service id if you routed to a +custom service). By default, the command will run forever: looking for new messages +on your transport and handling them. This command is called your "worker". + +Deploying to Production +~~~~~~~~~~~~~~~~~~~~~~~ + +On production, there are a few important things to think about: + +**Use Supervisor to keep your worker(s) running** + You'll want one or more "workers" running at all times. To do that, use a + process control system like :ref:`Supervisor `. + +**Don't Let Workers Run Forever** + Some services (like Doctrine's EntityManager) will consume more memory + over time. So, instead of allowing your worker to run forever, use a flag + like ``messenger:consume --limit=10`` to tell your worker to only handle 10 + messages before exiting (then Supervisor will create a new process). There + are also other options like ``--memory-limit=128M`` and ``--time-limit=3600``. + +**Restart Workers on Deploy** + Each time you deploy, you'll need to restart all your worker processes so + that they see the newly deployed code. To do this, run ``messenger:stop-workers`` + on deploy. This will signal to each worker that it should finish the message + it's currently handling and shut down gracefully. Then, Supervisor will create + new worker processes. The command uses the :ref:`app ` + cache internally - so make sure this is configured to use an adapter you like. + +Prioritized Transports +~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes certain types of messages should have a higher priority and be handled +before others. To make this possible, you can create multiple transports and route +different messages to them. For example: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + async_priority_high: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + options: + # queue_name is specific to the doctrine transport + queue_name: high + + # for AMQP send to a separate exchange then queue + #exchange: + # name: high + #queues: + # messages_high: ~ + # or redis try "group" + async_priority_low: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + options: + queue_name: low + + routing: + 'App\Message\SmsNotification': async_priority_low + 'App\Message\NewUserWelcomeEmail': async_priority_high + + .. code-block:: xml + + + + + + + + + + high + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + $container->loadFromExtension('framework', [ + 'messenger' => [ + 'transports' => [ + 'async_priority_high' => [ + 'dsn' => '%env(MESSENGER_TRANSPORT_DSN)%', + 'options' => [ + 'queue_name' => 'high', + ], + ], + 'async_priority_low' => [ + 'dsn' => '%env(MESSENGER_TRANSPORT_DSN)%', + 'options' => [ + 'queue_name' => 'low', + ], + ], + ], + 'routing' => [ + 'App\Message\SmsNotification' => 'async_priority_low', + 'App\Message\NewUserWelcomeEmail' => 'async_priority_high', + ], + ], + ]); + +You can then run individual workers for each transport or instruct one worker +to handle messages in a priority order: + +.. code-block:: terminal + + $ php bin/console messenger:consume async_priority_high async_priority_low + +The worker will always first look for messages waiting on ``async_priority_high``. If +there are none, *then* it will consume messages from ``async_priority_low``. + +.. _messenger-supervisor: + +Supervisor Configuration +~~~~~~~~~~~~~~~~~~~~~~~~ + +Supervisor is a great tool to guarantee that your worker process(es) is +*always* running (even if it closes due to failure, hitting a message limit +or thanks to ``messenger:stop-workers``). You can install it on Ubuntu, for +example, via: + +.. code-block:: terminal + + $ sudo apt-get install supervisor + +Supervisor configuration files typically live in a ``/etc/supervisor/conf.d`` +directory. For example, you can create a new ``messenger-worker.conf`` file +there to make sure that 2 instances of ``messenger:consume`` are running at all +times: + +.. code-block:: ini + + ;/etc/supervisor/conf.d/messenger-worker.conf + [program:messenger-consume] + command=php /path/to/your/app/bin/console messenger:consume async --time-limit=3600 + user=ubuntu + numprocs=2 + autostart=true + autorestart=true + process_name=%(program_name)s_%(process_num)02d + +Change the ``async`` argument to use the name of your transport (or transports) +and ``user`` to the Unix user on your server. Next, tell Supervisor to read your +config and start your workers: + +.. code-block:: terminal + + $ sudo supervisorctl reread + + $ sudo supervisorctl update + + $ sudo supervisorctl start messenger-consume:* + +See the `Supervisor docs`_ for more details. + +.. _messenger-retries-failures: + +Retries & Failures +------------------ + +If an exception is thrown while consuming a message from a transport it will +automatically be re-sent to the transport to be tried again. By default, a message +will be retried 3 times before being discarded or +:ref:`sent to the failure transport `. Each retry +will also be delayed, in case the failure was due to a temporary issue. All of +this is configurable for each transport: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + async_priority_high: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + + # default configuration + retry_strategy: + max_retries: 3 + # milliseconds delay + delay: 1000 + # causes the delay to be higher before each retry + # e.g. 1 second delay, 2 seconds, 4 seconds + multiplier: 2 + max_delay: 0 + # override all of this with a service that + # implements Symfony\Component\Messenger\Retry\RetryStrategyInterface + # service: null + +Avoiding Retrying +~~~~~~~~~~~~~~~~~ + +Sometimes handling a message might fail in a way that you *know* is permanent +and should not be retried. If you throw +:class:`Symfony\\Component\\Messenger\\Exception\\UnrecoverableMessageHandlingException`, +the message will not be retried. + +.. _messenger-failure-transport: + +Saving & Retrying Failed Messages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a message fails it is retried multiple times (``max_retries``) and then will +be discarded. To avoid this happening, you can instead configure a ``failure_transport``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + # after retrying, messages will be sent to the "failed" transport + failure_transport: failed + + transports: + # ... other transports + + failed: 'doctrine://default?queue_name=failed' + +In this example, if handling a message fails 3 times (default ``max_retries``), +it will then be sent to the ``failed`` transport. While you *can* use +``messenger:consume failed`` to consume this like a normal transport, you'll +usually want to manually view the messages in the failure transport and choose +to retry them: + +.. code-block:: terminal + + # see all messages in the failure transport + $ php bin/console messenger:failed:show + + # see details about a specific failure + $ php bin/console messenger:failed:show 20 -vv + + # view and retry messages one-by-one + $ php bin/console messenger:failed:retry -vv + + # retry specific messages + $ php bin/console messenger:failed:retry 20 30 --force + + # remove a message without retrying it + $ php bin/console messenger:failed:remove 20 + +If the message fails again, it will be re-sent back to the failure transport +due to the normal :ref:`retry rules `. Once the max +retry has been hit, the message will be discarded permanently. + +.. _messenger-transports-config: + +Transport Configuration +----------------------- + +Messenger supports a number of different transport types, each with their own +options. + +AMQP Transport +~~~~~~~~~~~~~~ + +The ``amqp`` transport configuration looks like this: + +.. code-block:: bash + + # .env + MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages + +To use Symfony's built-in AMQP transport, you need the AMQP PHP extension. + +.. note:: + + By default, the transport will automatically create any exchanges, queues and + binding keys that are needed. That can be disabled, but some functionality + may not work correctly (like delayed queues). + +The transport has a number of other options, including ways to configure +the exchange, queues binding keys and more. See the documentation on +:class:`Symfony\\Component\\Messenger\\Transport\\AmqpExt\\Connection`. + +You can also configure AMQP-specific settings on your message by adding +:class:`Symfony\\Component\\Messenger\\Transport\\AmqpExt\\AmqpStamp` to +your Envelope:: + + use Symfony\Component\Messenger\Transport\AmqpExt\AmqpStamp; + // ... + + $attributes = []; + $bus->dispatch(new SmsNotification(), [ + new AmqpStamp('custom-routing-key', AMQP_NOPARAM, $attributes) + ]); + +.. caution:: + + The consumers do not show up in an admin panel as this transport does not rely on + ``\AmqpQueue::consume()`` which is blocking. Having a blocking receiver makes + the ``--time-limit/--memory-limit`` options of the ``messenger:consume`` command as well as + the ``messenger:stop-workers`` command inefficient, as they all rely on the fact that + the receiver returns immediately no matter if it finds a message or not. The consume + worker is responsible for iterating until it receives a message to handle and/or until one + of the stop conditions is reached. Thus, the worker's stop logic cannot be reached if it + is stuck in a blocking call. + +Doctrine Transport +~~~~~~~~~~~~~~~~~~ + +The Doctrine transport can be used to store messages in a database table. + +.. code-block:: bash + + # .env + MESSENGER_TRANSPORT_DSN=doctrine://default + +The format is ``doctrine://``, in case you have multiple connections +and want to use one other than the "default". The transport will automatically create +a table named ``messenger_messages`` (this is configurable) when the transport is +first used. You can disable that with the ``auto_setup`` option and set the table +up manually by calling the ``messenger:setup-transports`` command. + +.. tip:: + + To avoid tools like Doctrine Migrations from trying to remove this table because + it's not part of your normal schema, you can set the ``schema_filter`` option: + + .. code-block:: yaml + + # config/packages/doctrine.yaml + doctrine: + dbal: + schema_filter: '~^(?!messenger_messages)~' + +The transport has a number of options: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + async_priority_high: "%env(MESSENGER_TRANSPORT_DSN)%?queue_name=high_priority" + async_normal: + dsn: "%env(MESSENGER_TRANSPORT_DSN)%" + options: + queue_name: normal_priority + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + $container->loadFromExtension('framework', [ + 'messenger' => [ + 'transports' => [ + 'async_priority_high' => 'dsn' => '%env(MESSENGER_TRANSPORT_DSN)%?queue_name=high_priority', + 'async_priority_low' => [ + 'dsn' => '%env(MESSENGER_TRANSPORT_DSN)%', + 'options' => [ + 'queue_name' => 'normal_priority' + ] + ], + ], + ], + ]); + +Options defined under ``options`` take precedence over ones defined in the DSN. + +================== =================================== ====================== + Option Description Default +================== =================================== ====================== +table_name Name of the table messenger_messages +queue_name Name of the queue (a column in the default + table, to use one table for + multiple transports) +redeliver_timeout Timeout before retrying a message 3600 + that's in the queue but in the + "handling" state (if a worker died + for some reason, this will occur, + eventually you should retry the + message) - in seconds. +auto_setup Whether the table should be created + automatically during send / get. true +================== =================================== ====================== + +Redis Transport +~~~~~~~~~~~~~~~ + +The Redis transport uses `streams`_ to queue messages. + +.. code-block:: bash + + # .env + MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages + # Full DSN Example + MESSENGER_TRANSPORT_DSN=redis://password@localhost:6379/messages/symfony/consumer?auto_setup=true&serializer=1&stream_max_entries=0&dbindex=0 + # Unix Socket Example + MESSENGER_TRANSPORT_DSN=redis:///var/run/redis.sock + +.. versionadded:: 5.1 + + The Unix socket DSN was introduced in Symfony 5.1. + +To use the Redis transport, you will need the Redis PHP extension (>=4.3) and +a running Redis server (^5.0). + +A number of options can be configured via the DSN or via the ``options`` key +under the transport in ``messenger.yaml``: + +================== ===================================== ========================= + Option Description Default +================== ===================================== ========================= +stream The Redis stream name messages +group The Redis consumer group name symfony +consumer Consumer name used in Redis consumer +auto_setup Create the Redis group automatically? true +auth The Redis password +serializer How to serialize the final payload ``Redis::SERIALIZER_PHP`` + in Redis (the + ``Redis::OPT_SERIALIZER`` option) +stream_max_entries The maximum number of entries which ``0`` (which means "no trimming") + the stream will be trimmed to. Set + it to a large enough number to + avoid losing pending messages +tls Enable TLS support for the connection false +================== ===================================== ========================= + +In Memory Transport +~~~~~~~~~~~~~~~~~~~ + +The ``in-memory`` transport does not actually delivery messages. Instead, it +holds them in memory during the request, which can be useful for testing. +For example, if you have an ``async_priority_normal`` transport, you could +override it in the ``test`` environment to use this transport: + +.. code-block:: yaml + + # config/packages/test/messenger.yaml + framework: + messenger: + transports: + async_priority_normal: 'in-memory:///' + +Then, while testing, messages will *not* be delivered to the real transport. +Even better, in a test, you can check that exactly one message was sent +during a request:: + + // tests/DefaultControllerTest.php + namespace App\Tests; + + use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + use Symfony\Component\Messenger\Transport\InMemoryTransport; + + class DefaultControllerTest extends WebTestCase + { + public function testSomething() + { + $client = static::createClient(); + // ... + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + /* @var InMemoryTransport $transport */ + $transport = self::$container->get('messenger.transport.async_priority_normal'); + $this->assertCount(1, $transport->getSent()); + } + } + +.. note:: + + All ``in-memory`` transports will be reset automatically after each test **in** + test classes extending + :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase` + or :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase`. + +Serializing Messages +~~~~~~~~~~~~~~~~~~~~ + +When messages are sent to (and received from) a transport, they're serialized +using PHP's native ``serialize()`` & ``unserialize()`` functions. You can change +this globally (or for each transport) to a service that implements +:class:`Symfony\\Component\\Messenger\\Transport\\Serialization\\SerializerInterface`: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + serializer: + default_serializer: messenger.transport.symfony_serializer + symfony_serializer: + format: json + context: { } + + transports: + async_priority_normal: + dsn: # ... + serializer: messenger.transport.symfony_serializer + +The ``messenger.transport.symfony_serializer`` is a built-in service that uses +the :doc:`Serializer component ` and can be configured in a few ways. +If you *do* choose to use the Symfony serializer, you can control the context +on a case-by-case basis via the :class:`Symfony\\Component\\Messenger\\Stamp\\SerializerStamp` +(see `Envelopes & Stamps`_). + +Customizing Handlers +-------------------- + +.. _messenger-handler-config: + +Manually Configuring Handlers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony will normally :ref:`find and register your handler automatically `. +But, you can also configure a handler manually - and pass it some extra config - +by tagging the handler service with ``messenger.message_handler`` + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + App\MessageHandler\SmsNotificationHandler: + tags: [messenger.message_handler] + + # or configure with options + tags: + - + name: messenger.message_handler + # only needed if can't be guessed by type-hint + handles: App\Message\SmsNotification + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + use App\Message\SmsNotification; + use App\MessageHandler\SmsNotificationHandler; + + $container->register(SmsNotificationHandler::class) + ->addTag('messenger.message_handler', [ + // only needed if can't be guessed by type-hint + 'handles' => SmsNotification::class, + ]); + + +Possible options to configure with tags are: + +* ``bus`` +* ``from_transport`` +* ``handles`` +* ``method`` +* ``priority`` + +Handler Subscriber & Options +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A handler class can handle multiple messages or configure itself by implementing +:class:`Symfony\\Component\\Messenger\\Handler\\MessageSubscriberInterface`:: + + // src/MessageHandler/SmsNotificationHandler.php + namespace App\MessageHandler; + + use App\Message\OtherSmsNotification; + use App\Message\SmsNotification; + use Symfony\Component\Messenger\Handler\MessageSubscriberInterface; + + class SmsNotificationHandler implements MessageSubscriberInterface + { + public function __invoke(SmsNotification $message) + { + // ... + } + + public function handleOtherSmsNotification(OtherSmsNotification $message) + { + // ... + } + + public static function getHandledMessages(): iterable + { + // handle this message on __invoke + yield SmsNotification::class; + + // also handle this message on handleOtherSmsNotification + yield OtherSmsNotification::class => [ + 'method' => 'handleOtherSmsNotification', + //'priority' => 0, + //'bus' => 'messenger.bus.default', + ]; + } + } + +Binding Handlers to Different Transports +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each message can have multiple handlers, and when a message is consumed +*all* of its handlers are called. But you can also configure a handler to only +be called when it's received from a *specific* transport. This allows you to +have a single message where each handler is called by a different "worker" +that's consuming a different transport. + +Suppose you have an ``UploadedImage`` message with two handlers: + +* ``ThumbnailUploadedImageHandler``: you want this to be handled by + a transport called ``image_transport`` + +* ``NotifyAboutNewUploadedImageHandler``: you want this to be handled + by a transport called ``async_priority_normal`` + +To do this, add the ``from_transport`` option to each handler. For example:: + + // src/MessageHandler/ThumbnailUploadedImageHandler.php + namespace App\MessageHandler; + + use App\Message\UploadedImage; + use Symfony\Component\Messenger\Handler\MessageSubscriberInterface; + + class ThumbnailUploadedImageHandler implements MessageSubscriberInterface + { + public function __invoke(UploadedImage $uploadedImage) + { + // do some thumbnailing + } + + public static function getHandledMessages(): iterable + { + yield UploadedImage::class => [ + 'from_transport' => 'image_transport', + ]; + } + } + +And similarly:: + + // src/MessageHandler/NotifyAboutNewUploadedImageHandler.php + // ... + + class NotifyAboutNewUploadedImageHandler implements MessageSubscriberInterface + { + // ... + + public static function getHandledMessages(): iterable + { + yield UploadedImage::class => [ + 'from_transport' => 'async_priority_normal', + ]; + } + } + +Then, make sure to "route" your message to *both* transports: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + async_priority_normal: # ... + image_transport: # ... + + routing: + # ... + 'App\Message\UploadedImage': [image_transport, async_priority_normal] + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + $container->loadFromExtension('framework', [ + 'messenger' => [ + 'transports' => [ + 'async_priority_normal' => '...', + 'image_transport' => '...', + ], + 'routing' => [ + 'App\Message\UploadedImage' => ['image_transport', 'async_priority_normal'] + ] + ], + ]); + +That's it! You can now consume each transport: + +.. code-block:: terminal + + # will only call ThumbnailUploadedImageHandler when handling the message + $ php bin/console messenger:consume image_transport -vv + + $ php bin/console messenger:consume async_priority_normal -vv + +.. caution:: + + If a handler does *not* have ``from_transport`` config, it will be executed + on *every* transport that the message is received from. + +Extending Messenger +------------------- + +Envelopes & Stamps +~~~~~~~~~~~~~~~~~~ + +A message can be any PHP object. Sometimes, you may need to configure something +extra about the message - like the way it should be handled inside AMQP or adding +a delay before the message should be handled. You can do that by adding a "stamp" +to your message:: + + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\MessageBusInterface; + use Symfony\Component\Messenger\Stamp\DelayStamp; + + public function index(MessageBusInterface $bus) + { + $bus->dispatch(new SmsNotification('...'), [ + // wait 5 seconds before processing + new DelayStamp(5000) + ]); + + // or explicitly create an Envelope + $bus->dispatch(new Envelope(new SmsNotification('...'), [ + new DelayStamp(5000) + ])); + + // ... + } + +Internally, each message is wrapped in an ``Envelope``, which holds the message +and stamps. You can create this manually or allow the message bus to do it. There +are a variety of different stamps for different purposes and they're used internally +to track information about a message - like the message bus that's handling it +or if it's being retried after failure. + +Middleware +~~~~~~~~~~ + +What happens when you dispatch a message to a message bus depends on its +collection of middleware and their order. By default, the middleware configured +for each bus looks like this: + +#. ``add_bus_name_stamp_middleware`` - adds a stamp to record which bus this + message was dispatched into; + +#. ``dispatch_after_current_bus``- see :doc:`/messenger/message-recorder`; + +#. ``failed_message_processing_middleware`` - processes messages that are being + retried via the :ref:`failure transport ` to make + them properly function as if they were being received from their original transport; + +#. Your own collection of middleware_; + +#. ``send_message`` - if routing is configured for the transport, this sends + messages to that transport and stops the middleware chain; + +#. ``handle_message`` - calls the message handler(s) for the given message. + +.. note:: + + These middleware names are actually shortcut names. The real service ids + are prefixed with ``messenger.middleware.`` (e.g. ``messenger.middleware.handle_message``). + +The middleware are executed when the message is dispatched but *also* again when +a message is received via the worker (for messages that were sent to a transport +to be handled asynchronously). Keep this in mind if you create your own middleware. + +You can add your own middleware to this list, or completely disable the default +middleware and *only* include your own: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + buses: + messenger.bus.default: + middleware: + # service ids that implement Symfony\Component\Messenger\Middleware\MiddlewareInterface + - 'App\Middleware\MyMiddleware' + - 'App\Middleware\AnotherMiddleware' + + #default_middleware: false + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + $container->loadFromExtension('framework', [ + 'messenger' => [ + 'buses' => [ + 'messenger.bus.default' => [ + 'middleware' => [ + 'App\Middleware\MyMiddleware', + 'App\Middleware\AnotherMiddleware', + ], + 'default_middleware' => false, + ], + ], + ], + ]); + + +.. note:: + + If a middleware service is abstract, a different instance of the service will + be created per bus. + +Middleware for Doctrine +~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.11 + + The following Doctrine middleware were introduced in DoctrineBundle 1.11. + +If you use Doctrine in your app, a number of optional middleware exist that you +may want to use: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + buses: + command_bus: + middleware: + # each time a message is handled, the Doctrine connection + # is "pinged" and reconnected if it's closed. Useful + # if your workers run for a long time and the database + # connection is sometimes lost + - doctrine_ping_connection + + # After handling, the Doctrine connection is closed, + # which can free up database connections in a worker, + # instead of keeping them open forever + - doctrine_close_connection + + # wraps all handlers in a single Doctrine transaction + # handlers do not need to call flush() and an error + # in any handler will cause a rollback + - doctrine_transaction + + # or pass a different entity manager to any + #- doctrine_transaction: ['custom'] + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + $container->loadFromExtension('framework', [ + 'messenger' => [ + 'buses' => [ + 'command_bus' => [ + 'middleware' => [ + 'doctrine_transaction', + 'doctrine_ping_connection', + 'doctrine_close_connection', + // Using another entity manager + ['id' => 'doctrine_transaction', 'arguments' => ['custom']], + ], + ], + ], + ], + ]); + +Messenger Events +~~~~~~~~~~~~~~~~ + +In addition to middleware, Messenger also dispatches several events. You can +:doc:`create an event listener ` to hook into various parts +of the process. For each, the event class is the event name: + +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerStartedEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageReceivedEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\SendMessageToTransportsEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageFailedEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageHandledEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerRunningEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerStoppedEvent` + +Multiple Buses, Command & Event Buses +------------------------------------- + +Messenger gives you a single message bus service by default. But, you can configure +as many as you want, creating "command", "query" or "event" buses and controlling +their middleware. See :doc:`/messenger/multiple_buses`. + +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /messenger/* + +.. _`Enqueue's transport`: https://github.com/sroze/messenger-enqueue-transport +.. _`streams`: https://redis.io/topics/streams-intro +.. _`Supervisor docs`: http://supervisord.org/ diff --git a/messenger/custom-transport.rst b/messenger/custom-transport.rst new file mode 100644 index 00000000000..e0fbcb3ca23 --- /dev/null +++ b/messenger/custom-transport.rst @@ -0,0 +1,219 @@ +How to Create Your own Messenger Transport +========================================== + +Once you have written your transport's sender and receiver, you can register your +transport factory to be able to use it via a DSN in the Symfony application. + +Create your Transport Factory +----------------------------- + +You need to give FrameworkBundle the opportunity to create your transport from a +DSN. You will need a transport factory:: + + use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; + use Symfony\Component\Messenger\Transport\Sender\SenderInterface; + use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; + use Symfony\Component\Messenger\Transport\TransportFactoryInterface; + use Symfony\Component\Messenger\Transport\TransportInterface; + + class YourTransportFactory implements TransportFactoryInterface + { + public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface + { + return new YourTransport(/* ... */); + } + + public function supports(string $dsn, array $options): bool + { + return 0 === strpos($dsn, 'my-transport://'); + } + } + +The transport object needs to implement the +:class:`Symfony\\Component\\Messenger\\Transport\\TransportInterface` +(which combines the :class:`Symfony\\Component\\Messenger\\Transport\\Sender\\SenderInterface` +and :class:`Symfony\\Component\\Messenger\\Transport\\Receiver\\ReceiverInterface`). +Here is a simplified example of a database transport:: + + use Ramsey\Uuid\Uuid; + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp; + use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer; + use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; + use Symfony\Component\Messenger\Transport\TransportInterface; + + class YourTransport implements TransportInterface + { + private $db; + private $serializer; + + /** + * @param FakeDatabase $db is used for demo purposes. It is not a real class. + */ + public function __construct(FakeDatabase $db, SerializerInterface $serializer = null) + { + $this->db = $db; + $this->serializer = $serializer ?? new PhpSerializer(); + } + + public function get(): iterable + { + // Get a message from "my_queue" + $row = $this->db->createQuery( + 'SELECT * + FROM my_queue + WHERE (delivered_at IS NULL OR delivered_at < :redeliver_timeout) + AND handled = FALSE' + ) + ->setParameter('redeliver_timeout', new DateTimeImmutable('-5minutes')) + ->getOneOrNullResult(); + + if (null === $row) { + return []; + } + + $envelope = $this->serializer->decode([ + 'body' => $row['envelope'], + ]); + + return [$envelope->with(new TransportMessageIdStamp($row['id']))]; + } + + public function ack(Envelope $envelope): void + { + $stamp = $envelope->last(TransportMessageIdStamp::class); + if (!$stamp instanceof TransportMessageIdStamp) { + throw new \LogicException('No TransportMessageIdStamp found on the Envelope.'); + } + + // Mark the message as "handled" + $this->db->createQuery('UPDATE my_queue SET handled = TRUE WHERE id = :id') + ->setParameter('id', $stamp->getId()) + ->execute(); + } + + public function reject(Envelope $envelope): void + { + $stamp = $envelope->last(TransportMessageIdStamp::class); + if (!$stamp instanceof TransportMessageIdStamp) { + throw new \LogicException('No TransportMessageIdStamp found on the Envelope.'); + } + + // Delete the message from the "my_queue" table + $this->db->createQuery('DELETE FROM my_queue WHERE id = :id') + ->setParameter('id', $stamp->getId()) + ->execute(); + } + + public function send(Envelope $envelope): Envelope + { + $encodedMessage = $this->serializer->encode($envelope); + $uuid = Uuid::uuid4()->toString(); + + // Add a message to the "my_queue" table + $this->db->createQuery( + 'INSERT INTO my_queue (id, envelope, delivered_at, handled) + VALUES (:id, :envelope, NULL, FALSE)' + ) + ->setParameters([ + 'id' => $uuid, + 'envelope' => $encodedMessage['body'], + ]) + ->execute(); + + return $envelope->with(new TransportMessageIdStamp($uuid)); + } + } + +The implementation above is not runnable code but illustrates how a +:class:`Symfony\\Component\\Messenger\\Transport\\TransportInterface` could +be implemented. For real implementations see :class:`Symfony\\Component\\Messenger\\Transport\\InMemoryTransport` +and :class:`Symfony\\Component\\Messenger\\Transport\\Doctrine\\DoctrineReceiver`. + +Register your Factory +--------------------- + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + Your\Transport\YourTransportFactory: + tags: [messenger.transport_factory] + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + use Your\Transport\YourTransportFactory; + + $container->register(YourTransportFactory::class) + ->setTags(['messenger.transport_factory']); + +Use your Transport +------------------ + +Within the ``framework.messenger.transports.*`` configuration, create your +named transport using your own DSN: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + yours: 'my-transport://...' + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + $container->loadFromExtension('framework', [ + 'messenger' => [ + 'transports' => [ + 'yours' => 'my-transport://...', + ], + ], + ]); + +In addition of being able to route your messages to the ``yours`` sender, this +will give you access to the following services: + +#. ``messenger.sender.yours``: the sender; +#. ``messenger.receiver.yours``: the receiver. diff --git a/messenger/handler_results.rst b/messenger/handler_results.rst new file mode 100644 index 00000000000..dc4c1fd0821 --- /dev/null +++ b/messenger/handler_results.rst @@ -0,0 +1,102 @@ +.. index:: + single: Messenger; Getting results / Working with command & query buses + +Getting Results from your Handler +================================= + +When a message is handled, the :class:`Symfony\\Component\\Messenger\\Middleware\\HandleMessageMiddleware` +adds a :class:`Symfony\\Component\\Messenger\\Stamp\\HandledStamp` for each object that handled the message. +You can use this to get the value returned by the handler(s):: + + use Symfony\Component\Messenger\MessageBusInterface; + use Symfony\Component\Messenger\Stamp\HandledStamp; + + $envelope = $messageBus->dispatch(SomeMessage()); + + // get the value that was returned by the last message handler + $handledStamp = $envelope->last(HandledStamp::class); + $handledStamp->getResult(); + + // or get info about all of handlers + $handledStamps = $envelope->all(HandledStamp::class); + +Working with Command & Query Buses +---------------------------------- + +The Messenger component can be used in CQRS architectures where command & query +buses are central pieces of the application. Read Martin Fowler's +`article about CQRS`_ to learn more and +:doc:`how to configure multiple buses `. + +As queries are usually synchronous and expected to be handled once, +getting the result from the handler is a common need. + +A :class:`Symfony\\Component\\Messenger\\HandleTrait` exists to get the result +of the handler when processing synchronously. It also ensures that exactly one +handler is registered. The ``HandleTrait`` can be used in any class that has a +``$messageBus`` property:: + + // src/Action/ListItems.php + namespace App\Action; + + use App\Message\ListItemsQuery; + use App\MessageHandler\ListItemsQueryResult; + use Symfony\Component\Messenger\HandleTrait; + use Symfony\Component\Messenger\MessageBusInterface; + + class ListItems + { + use HandleTrait; + + public function __construct(MessageBusInterface $messageBus) + { + $this->messageBus = $messageBus; + } + + public function __invoke() + { + $result = $this->query(new ListItemsQuery(/* ... */)); + + // Do something with the result + // ... + } + + // Creating such a method is optional, but allows type-hinting the result + private function query(ListItemsQuery $query): ListItemsResult + { + return $this->handle($query); + } + } + +Hence, you can use the trait to create command & query bus classes. +For example, you could create a special ``QueryBus`` class and inject it +wherever you need a query bus behavior instead of the ``MessageBusInterface``:: + + // src/MessageBus/QueryBus.php + namespace App\MessageBus; + + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\HandleTrait; + use Symfony\Component\Messenger\MessageBusInterface; + + class QueryBus + { + use HandleTrait; + + public function __construct(MessageBusInterface $messageBus) + { + $this->messageBus = $messageBus; + } + + /** + * @param object|Envelope $query + * + * @return mixed The handler returned value + */ + public function query($query) + { + return $this->handle($query); + } + } + +.. _`article about CQRS`: https://martinfowler.com/bliki/CQRS.html diff --git a/messenger/message-recorder.rst b/messenger/message-recorder.rst new file mode 100644 index 00000000000..f5ab29a1ede --- /dev/null +++ b/messenger/message-recorder.rst @@ -0,0 +1,135 @@ +.. index:: + single: Messenger; Record messages; Transaction messages + +Transactional Messages: Handle New Messages After Handling is Done +================================================================== + +A message handler can ``dispatch`` new messages during execution, to either the +same or a different bus (if the application has +:doc:`multiple buses `). Any errors or exceptions that +occur during this process can have unintended consequences, such as: + +- If using the ``DoctrineTransactionMiddleware`` and a dispatched message throws + an exception, then any database transactions in the original handler will be + rolled back. +- If the message is dispatched to a different bus, then the dispatched message + will be handled even if some code later in the current handler throws an + exception. + +An Example ``RegisterUser`` Process +----------------------------------- + +Let's take the example of an application with both a *command* and an *event* +bus. The application dispatches a command named ``RegisterUser`` to the command +bus. The command is handled by the ``RegisterUserHandler`` which creates a +``User`` object, stores that object to a database and dispatches a +``UserRegistered`` message to the event bus. + +There are many handlers to the ``UserRegistered`` message, one handler may send +a welcome email to the new user. We are using the ``DoctrineTransactionMiddleware`` +to wrap all database queries in one database transaction. + +**Problem 1:** If an exception is thrown when sending the welcome email, then +the user will not be created because the ``DoctrineTransactionMiddleware`` will +rollback the Doctrine transaction, in which the user has been created. + +**Problem 2:** If an exception is thrown when saving the user to the database, +the welcome email is still sent because it is handled asynchronously. + +DispatchAfterCurrentBusMiddleware Middleware +-------------------------------------------- + +For many applications, the desired behavior is to *only* handle messages that +are dispatched by a handler once that handler has fully finished. This can be by +using the ``DispatchAfterCurrentBusMiddleware`` and adding a +``DispatchAfterCurrentBusStamp`` stamp to +`the message Envelope `_:: + + namespace App\Messenger\CommandHandler; + + use App\Entity\User; + use App\Messenger\Command\RegisterUser; + use App\Messenger\Event\UserRegistered; + use Doctrine\ORM\EntityManagerInterface; + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\MessageBusInterface; + use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp; + + class RegisterUserHandler + { + private $eventBus; + private $em; + + public function __construct(MessageBusInterface $eventBus, EntityManagerInterface $em) + { + $this->eventBus = $eventBus; + $this->em = $em; + } + + public function __invoke(RegisterUser $command) + { + $user = new User($command->getUuid(), $command->getName(), $command->getEmail()); + $this->em->persist($user); + + // The DispatchAfterCurrentBusStamp marks the event message to be handled + // only if this handler does not throw an exception. + + $event = new UserRegistered($command->getUuid()); + $this->eventBus->dispatch( + (new Envelope($event)) + ->with(new DispatchAfterCurrentBusStamp()) + ); + + // ... + } + } + +.. code-block:: php + + namespace App\Messenger\EventSubscriber; + + use App\Entity\User; + use App\Messenger\Event\UserRegistered; + use Doctrine\ORM\EntityManagerInterface; + use Symfony\Component\Mailer\MailerInterface; + use Symfony\Component\Mime\RawMessage; + + class WhenUserRegisteredThenSendWelcomeEmail + { + private $mailer; + private $em; + + public function __construct(MailerInterface $mailer, EntityManagerInterface $em) + { + $this->mailer = $mailer; + $this->em = $em; + } + + public function __invoke(UserRegistered $event) + { + $user = $this->em->getRepository(User::class)->find($event->getUuid()); + + $this->mailer->send(new RawMessage('Welcome '.$user->getFirstName())); + } + } + +This means that the ``UserRegistered`` message would not be handled until +*after* the ``RegisterUserHandler`` had completed and the new ``User`` was +persisted to the database. If the ``RegisterUserHandler`` encounters an +exception, the ``UserRegistered`` event will never be handled. And if an +exception is thrown while sending the welcome email, the Doctrine transaction +will not be rolled back. + +.. note:: + + If ``WhenUserRegisteredThenSendWelcomeEmail`` throws an exception, that + exception will be wrapped into a ``DelayedMessageHandlingException``. Using + ``DelayedMessageHandlingException::getExceptions`` will give you all + exceptions that are thrown while handing a message with the + ``DispatchAfterCurrentBusStamp``. + +The ``dispatch_after_current_bus`` middleware is enabled by default. If you're +configuring your middleware manually, be sure to register +``dispatch_after_current_bus`` before ``doctrine_transaction`` in the middleware +chain. Also, the ``dispatch_after_current_bus`` middleware must be loaded for +*all* of the buses being used. diff --git a/messenger/multiple_buses.rst b/messenger/multiple_buses.rst new file mode 100644 index 00000000000..a60a3e3f60b --- /dev/null +++ b/messenger/multiple_buses.rst @@ -0,0 +1,252 @@ +.. index:: + single: Messenger; Multiple buses + +Multiple Buses +============== + +A common architecture when building applications is to separate commands from +queries. Commands are actions that do something and queries fetch data. This +is called CQRS (Command Query Responsibility Segregation). See Martin Fowler's +`article about CQRS`_ to learn more. This architecture could be used together +with the Messenger component by defining multiple buses. + +A **command bus** is a little different from a **query bus**. For example, command +buses usually don't provide any results and query buses are rarely asynchronous. +You can configure these buses and their rules by using middleware. + +It might also be a good idea to separate actions from reactions by introducing +an **event bus**. The event bus could have zero or more subscribers. + +.. configuration-block:: + + .. code-block:: yaml + + framework: + messenger: + # The bus that is going to be injected when injecting MessageBusInterface + default_bus: command.bus + buses: + command.bus: + middleware: + - validation + - doctrine_transaction + query.bus: + middleware: + - validation + event.bus: + default_middleware: allow_no_handlers + middleware: + - validation + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/messenger.php + $container->loadFromExtension('framework', [ + 'messenger' => [ + // The bus that is going to be injected when injecting MessageBusInterface + 'default_bus' => 'command.bus', + 'buses' => [ + 'command.bus' => [ + 'middleware' => [ + 'validation', + 'doctrine_transaction', + ], + ], + 'query.bus' => [ + 'middleware' => [ + 'validation', + ], + ], + 'event.bus' => [ + 'default_middleware' => 'allow_no_handlers', + 'middleware' => [ + 'validation', + ], + ], + ], + ], + ]); + +This will create three new services: + +* ``command.bus``: autowireable with the :class:`Symfony\\Component\\Messenger\\MessageBusInterface` + type-hint (because this is the ``default_bus``); + +* ``query.bus``: autowireable with ``MessageBusInterface $queryBus``; + +* ``event.bus``: autowireable with ``MessageBusInterface $eventBus``. + +Restrict Handlers per Bus +------------------------- + +By default, each handler will be available to handle messages on *all* +of your buses. To prevent dispatching a message to the wrong bus without an error, +you can restrict each handler to a specific bus using the ``messenger.message_handler`` tag: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + App\MessageHandler\SomeCommandHandler: + tags: [{ name: messenger.message_handler, bus: command.bus }] + # prevent handlers from being registered twice (or you can remove + # the MessageHandlerInterface that autoconfigure uses to find handlers) + autoconfigure: false + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + $container->services() + ->set(App\MessageHandler\SomeCommandHandler::class) + ->tag('messenger.message_handler', ['bus' => 'command.bus']); + +This way, the ``App\MessageHandler\SomeCommandHandler`` handler will only be +known by the ``command.bus`` bus. + +You can also automatically add this tag to a number of classes by following +a naming convention and registering all of the handler services by name with +the correct tag: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + + # put this after the "App\" line that registers all your services + command_handlers: + namespace: App\MessageHandler\ + resource: '%kernel.project_dir%/src/MessageHandler/*CommandHandler.php' + autoconfigure: false + tags: + - { name: messenger.message_handler, bus: command.bus } + + query_handlers: + namespace: App\MessageHandler\ + resource: '%kernel.project_dir%/src/MessageHandler/*QueryHandler.php' + autoconfigure: false + tags: + - { name: messenger.message_handler, bus: query.bus } + + .. code-block:: xml + + + + + + + + + +
+ + + + + +
+ + .. code-block:: php + + // config/services.php + + // Command handlers + $container->services() + ->load('App\MessageHandler\\', '%kernel.project_dir%/src/MessageHandler/*CommandHandler.php') + ->autoconfigure(false) + ->tag('messenger.message_handler', ['bus' => 'command.bus']); + + // Query handlers + $container->services() + ->load('App\MessageHandler\\', '%kernel.project_dir%/src/MessageHandler/*QueryHandler.php') + ->autoconfigure(false) + ->tag('messenger.message_handler', ['bus' => 'query.bus']); + +Debugging the Buses +------------------- + +The ``debug:messenger`` command lists available messages & handlers per bus. +You can also restrict the list to a specific bus by providing its name as argument. + +.. code-block:: terminal + + $ php bin/console debug:messenger + + Messenger + ========= + + command.bus + ----------- + + The following messages can be dispatched: + + --------------------------------------------------------------------------------------- + App\Message\DummyCommand + handled by App\MessageHandler\DummyCommandHandler + App\Message\MultipleBusesMessage + handled by App\MessageHandler\MultipleBusesMessageHandler + --------------------------------------------------------------------------------------- + + query.bus + --------- + + The following messages can be dispatched: + + --------------------------------------------------------------------------------------- + App\Message\DummyQuery + handled by App\MessageHandler\DummyQueryHandler + App\Message\MultipleBusesMessage + handled by App\MessageHandler\MultipleBusesMessageHandler + --------------------------------------------------------------------------------------- + +.. _article about CQRS: https://martinfowler.com/bliki/CQRS.html diff --git a/migration.rst b/migration.rst new file mode 100644 index 00000000000..144fe93d08d --- /dev/null +++ b/migration.rst @@ -0,0 +1,467 @@ +.. index:: + single: Migration + +Migrating an Existing Application to Symfony +============================================ + +When you have an existing application that was not built with Symfony, +you might want to move over parts of that application without rewriting +the existing logic completely. For those cases there is a pattern called +`Strangler Application`_. The basic idea of this pattern is to create a +new application that gradually takes over functionality from an existing +application. This migration approach can be implemented with Symfony in +various ways and has some benefits over a rewrite such as being able +to introduce new features in the existing application and reducing risk +by avoiding a "big bang"-release for the new application. + +.. admonition:: Screencast + :class: screencast + + The topic of migrating from an existing application towards Symfony is + sometimes discussed during conferences. For example the talk + `Modernizing with Symfony`_ reiterates some of the points from this page. + +Prerequisites +------------- + +Before you start introducing Symfony to the existing application, you have to +ensure certain requirements are met by your existing application and +environment. Making the decisions and preparing the environment before +starting the migration process is crucial for its success. + +.. note:: + + The following steps do not require you to have the new Symfony + application in place and in fact it might be safer to introduce these + changes beforehand in your existing application. + +Choosing the Target Symfony Version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Most importantly, this means that you will have to decide which version you +are aiming to migrate to, either a current stable release or the long +term support version (LTS). The main difference is, how frequently you +will need to upgrade in order to use a supported version. In the context +of a migration, other factors, such as the supported PHP-version or +support for libraries/bundles you use, may have a strong impact as well. +Using the most recent, stable release will likely give you more features, +but it will also require you to update more frequently to ensure you will +get support for bug fixes and security patches and you will have to work +faster on fixing deprecations to be able to upgrade. + +.. tip:: + + When upgrading to Symfony you might be tempted to also use + :ref:`Flex `. Please keep in mind that it primarily + focuses on bootstrapping a new Symfony application according to best + practices regarding the directory structure. When you work in the + constraints of an existing application you might not be able to + follow these constraints, making Flex less useful. + +First of all your environment needs to be able to support the minimum +requirements for both applications. In other words, when the Symfony +release you aim to use requires PHP 7.1 and your existing application +does not yet support this PHP version, you will probably have to upgrade +your legacy project. Use the ``check:requirements`` command to check if your +server meets the :ref:`technical requirements for running Symfony applications ` +and compare them with your current application's environment to make sure you +are able to run both applications on the same system. Having a test +system, that is as close to the production environment as possible, +where you can just install a new Symfony project next to the existing one +and check if it is working will give you an even more reliable result. + +.. tip:: + + If your current project is running on an older PHP version such as + PHP 5.x upgrading to a recent version will give you a performance + boost without having to change your code. + +Setting up Composer +~~~~~~~~~~~~~~~~~~~ + +Another point you will have to look out for is conflicts between +dependencies in both applications. This is especially important if your +existing application already uses Symfony components or libraries commonly +used in Symfony applications such as Doctrine ORM, Swiftmailer or Twig. +A good way for ensuring compatibility is to use the same ``composer.json`` +for both project's dependencies. + +Once you have introduced composer for managing your project's dependencies +you can use its autoloader to ensure you do not run into any conflicts due +to custom autoloading from your existing framework. This usually entails +adding an `autoload`_-section to your ``composer.json`` and configuring it +based on your application and replacing your custom logic with something +like this:: + + require __DIR__.'/vendor/autoload.php'; + +Removing Global State from the Legacy Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In older PHP applications it was quite common to rely on global state and +even mutate it during runtime. This might have side effects on the newly +introduced Symfony application. In other words code relying on globals +in the existing application should be refactored to allow for both systems +to work simultaneously. Since relying on global state is considered an +anti-pattern nowadays you might want to start working on this even before +doing any integration. + +Setting up the Environment +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There might be additional steps you need to take depending on the libraries +you use, the original framework your project is based on and most importantly +the age of the project as PHP itself underwent many improvements throughout +the years that your code might not have caught on to, yet. As long as both +your existing code and a new Symfony project can run in parallel on the +same system you are on a good way. All these steps do not require you to +introduce Symfony just yet and will already open up some opportunities for +modernizing your existing code. + +Establishing a Safety Net for Regressions +----------------------------------------- + +Before you can safely make changes to the existing code, you must ensure that +nothing will break. One reason for choosing to migrate is making sure that the +application is in a state where it can run at all times. The best way for +ensuring a working state is to establish automated tests. + +It is quite common for an existing application to either not have a test suite +at all or have low code coverage. Introducing unit tests for this code is +likely not cost effective as the old code might be replaced with functionality +from Symfony components or might be adapted to the new application. +Additionally legacy code tends to be hard to write tests for making the process +slow and cumbersome. + +Instead of providing low level tests, that ensure each class works as expected, it +might makes sense to write high level tests ensuring that at least anything user +facing works on at least a superficial level. These kinds of tests are commonly +called End-to-End tests, because they cover the whole application from what the +user sees in the browser down to the very code that is being run and connected +services like a database. To automate this you have to make sure that you can +get a test instance of your system running as easily as possible and making +sure that external systems do not change your production environment, e.g. +provide a separate test database with (anonymized) data from a production +system or being able to setup a new schema with a basic dataset for your test +environment. Since these tests do not rely as much on isolating testable code +and instead look at the interconnected system, writing them is usually easier +and more productive when doing a migration. You can then limit your effort on +writing lower level tests on parts of the code that you have to change or +replace in the new application making sure it is testable right from the start. + +There are tools aimed at End-to-End testing you can use such as +`Symfony Panther`_ or you can write :doc:`functional tests ` +in the new Symfony application as soon as the initial setup is completed. +For example you can add so called Smoke Tests, which only ensure a certain +path is accessible by checking the HTTP status code returned or looking for +a text snippet from the page. + +Introducing Symfony to the Existing Application +----------------------------------------------- + +The following instructions only provide an outline of common tasks for +setting up a Symfony application that falls back to a legacy application +whenever a route is not accessible. Your mileage may vary and likely you +will need to adjust some of this or even provide additional configuration +or retrofitting to make it work with your application. This guide is not +supposed to be comprehensive and instead aims to be a starting point. + +.. tip:: + + If you get stuck or need additional help you can reach out to the + :doc:`Symfony community ` whenever you need + concrete feedback on an issue you are facing. + +Booting Symfony in a Front Controller +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When looking at how a typical PHP application is bootstrapped there are +two major approaches. Nowadays most frameworks provide a so called +front controller which acts as an entrypoint. No matter which URL-path +in your application you are going to, every request is being sent to +this front controller, which then determines which parts of your +application to load, e.g. which controller and action to call. This is +also the approach that Symfony takes with ``public/index.php`` being +the front controller. Especially in older applications it was common +that different paths were handled by different PHP files. + +In any case you have to create a ``public/index.php`` that will start +your Symfony application by either copying the file from the +``FrameworkBundle``-recipe or by using Flex and requiring the +FrameworkBundle. You will also likely have to update you web server +(e.g. Apache or nginx) to always use this front controller. You can +look at :doc:`Web Server Configuration ` +for examples on how this might look. For example when using Apache you can +use Rewrite Rules to ensure PHP files are ignored and instead only index.php +is called: + +.. code-block:: apache + + RewriteEngine On + + RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ + RewriteRule ^(.*) - [E=BASE:%1] + + RewriteCond %{ENV:REDIRECT_STATUS} ^$ + RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L] + + RewriteRule ^index\.php - [L] + + RewriteCond %{REQUEST_FILENAME} -f + RewriteCond %{REQUEST_FILENAME} !^.+\.php$ + RewriteRule ^ - [L] + + RewriteRule ^ %{ENV:BASE}/index.php [L] + +This change will make sure that from now on your Symfony application is +the first one handling all requests. The next step is to make sure that +your existing application is started and taking over whenever Symfony +can not yet handle a path previously managed by the existing application. + +From this point, many tactics are possible and every project requires its +unique approach for migration. This guide shows two examples of commonly used +approaches, which you can use as a base for your own approach: + +* `Front Controller with Legacy Bridge`_, which leaves the legacy application + untouched and allows to migrate it in phases to the Symfony application. +* `Legacy Route Loader`_, where the legacy application is integrated in phases + into Symfony, with a fully integrated final result. + +Front Controller with Legacy Bridge +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once you have a running Symfony application that takes over all requests, +falling back to your legacy application is done by extending the original front +controller script with some logic for going to your legacy system. The file +could look something like this:: + + // public/index.php + use App\Kernel; + use App\LegacyBridge; + use Symfony\Component\Debug\Debug; + use Symfony\Component\HttpFoundation\Request; + + require dirname(__DIR__).'/config/bootstrap.php'; + + /* + * The kernel will always be available globally, allowing you to + * access it from your existing application and through it the + * service container. This allows for introducing new features in + * the existing application. + */ + global $kernel; + + if ($_SERVER['APP_DEBUG']) { + umask(0000); + + Debug::enable(); + } + + if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) { + Request::setTrustedProxies( + explode(',', $trustedProxies), + Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST + ); + } + + if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false) { + Request::setTrustedHosts([$trustedHosts]); + } + + $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG'], dirname(__DIR__)); + $request = Request::createFromGlobals(); + $response = $kernel->handle($request); + + /* + * LegacyBridge will take care of figuring out whether to boot up the + * existing application or to send the Symfony response back to the client. + */ + $scriptFile = LegacyBridge::prepareLegacyScript($request, $response, __DIR__); + if ($scriptFile !== null) { + require $scriptFile; + } else { + $response->send(); + } + $kernel->terminate($request, $response); + +There are 2 major deviations from the original file: + +Line 15 + First of all, ``$kernel`` is made globally available. This allows you to use + Symfony features inside your existing application and gives access to + services configured in our Symfony application. This helps you prepare your + own code to work better within the Symfony application before you transition + it over. For instance, by replacing outdated or redundant libraries with + Symfony components. + +Line 38 - 47 + Instead of sending the Symfony response directly, a ``LegacyBridge`` is + called to decide whether the legacy application should be booted and used to + create the response instead. + +This legacy bridge is responsible for figuring out which file should be loaded +in order to process the old application logic. This can either be a front +controller similar to Symfony's ``public/index.php`` or a specific script file +based on the current route. The basic outline of this LegacyBridge could look +somewhat like this:: + + // src/LegacyBridge.php + namespace App; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + class LegacyBridge + { + public static function prepareLegacyScript(Request $request, Response $response, string $publicDirectory): string + { + // If Symfony successfully handled the route, you do not have to do anything. + if (false === $response->isNotFound()) { + return; + } + + // Figure out how to map to the needed script file + // from the existing application and possibly (re-)set + // some env vars. + $legacyScriptFilename = ...; + + return $legacyScriptFilename; + } + } + +This is the most generic approach you can take, that is likely to work +no matter what your previous system was. You might have to account for +certain "quirks", but since your original application is only started +after Symfony finished handling the request you reduced the chances +for side effects and any interference. + +Since the old script is called in the global variable scope it will reduce side +effects on the old code which can sometimes require variables from the global +scope. At the same time, because your Symfony application will always be +booted first, you can access the container via the ``$kernel`` variable and +then fetch any service (using :method:`Symfony\\Component\\HttpKernel\\KernelInterface::getContainer`). +This can be helpful if you want to introduce new features to your legacy +application, without switching over the whole action to the new application. +For example, you could now use the Symfony Translator in your old application +or instead of using your old database logic, you could use Doctrine to refactor +old queries. This will also allow you to incrementally improve the legacy code +making it easier to transition it over to the new Symfony application. + +The major downside is, that both systems are not well integrated +into each other leading to some redundancies and possibly duplicated code. +For example, since the Symfony application is already done handling the +request you can not take advantage of kernel events or utilize Symfony's +routing for determining which legacy script to call. + +Legacy Route Loader +~~~~~~~~~~~~~~~~~~~ + +The major difference to the LegacyBridge-approach from before is, that the +logic is moved inside the Symfony application. It removes some of the +redundancies and allows us to also interact with parts of the legacy +application from inside Symfony, instead of just the other way around. + +.. tip:: + + The following route loader is just a generic example that you might + have to tweak for your legacy application. You can familiarize + yourself with the concepts by reading up on it in :doc:`Routing `. + +The legacy route loader is :doc:`a custom route loader `. +The legacy route loader has a similar functionality as the previous +LegacyBridge, but it is a service that is registered inside Symfony's Routing +component:: + + // src/Legacy/LegacyRouteLoader.php + namespace App\Legacy; + + use Symfony\Component\Config\Loader\Loader; + use Symfony\Component\Routing\Route; + use Symfony\Component\Routing\RouteCollection; + + class LegacyRouteLoader extends Loader + { + // ... + + public function load($resource, $type = null) + { + $collection = new RouteCollection(); + $finder = new Finder(); + $finder->files()->name('*.php'); + + /** @var SplFileInfo $legacyScriptFile */ + foreach ($finder->in($this->webDir) as $legacyScriptFile) { + // This assumes all legacy files use ".php" as extension + $filename = basename($legacyScriptFile->getRelativePathname(), '.php'); + $routeName = sprintf('app.legacy.%s', str_replace('/', '__', $filename)); + + $collection->add($routeName, new Route($legacyScriptFile->getRelativePathname(), [ + '_controller' => 'App\Controller\LegacyController::loadLegacyScript', + 'requestPath' => '/' . $legacyScriptFile->getRelativePathname(), + 'legacyScript' => $legacyScriptFile->getPathname(), + ])); + } + + return $collection; + } + } + +You will also have to register the loader in your application's +``routing.yaml`` as described in the documentation for +:doc:`Custom Route Loaders `. +Depending on your configuration, you might also have to tag the service with +``routing.loader``. Afterwards you should be able to see all the legacy routes +in your route configuration, e.g. when you call the ``debug:router``-command: + +.. code-block:: terminal + + $ php bin/console debug:router + +In order to use these routes you will need to create a controller that handles +these routes. You might have noticed the ``_controller`` attribute in the +previous code example, which tells Symfony which Controller to call whenever it +tries to access one of our legacy routes. The controller itself can then use the +other route attributes (i.e. ``requestPath`` and ``legacyScript``) to determine +which script to call and wrap the output in a response class:: + + // src/Controller/LegacyController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\StreamedResponse; + + class LegacyController + { + public function loadLegacyScript(string $requestPath, string $legacyScript) + { + return new StreamedResponse( + function () use ($requestPath, $legacyScript) { + $_SERVER['PHP_SELF'] = $requestPath; + $_SERVER['SCRIPT_NAME'] = $requestPath; + $_SERVER['SCRIPT_FILENAME'] = $legacyScript; + + chdir(dirname($legacyScript)); + + require $legacyScript; + } + ); + } + } + +This controller will set some server variables that might be needed by +the legacy application. This will simulate the legacy script being called +directly, in case it relies on these variables (e.g. when determining +relative paths or file names). Finally the action requires the old script, +which essentially calls the original script as before, but it runs inside +our current application scope, instead of the global scope. + +There are some risks to this approach, as it is no longer run in the global +scope. However, since the legacy code now runs inside a controller action, you gain +access to many functionalities from the new Symfony application, including the +chance to use Symfony's event lifecycle. For instance, this allows you to +transition the authentication and authorization of the legacy application over +to the Symfony application using the Security component and its firewalls. + +.. _`Strangler Application`: https://www.martinfowler.com/bliki/StranglerApplication.html +.. _`autoload`: https://getcomposer.org/doc/04-schema.md#autoload +.. _`Modernizing with Symfony`: https://youtu.be/YzyiZNY9htQ +.. _`Symfony Panther`: https://github.com/symfony/panther diff --git a/page_creation.rst b/page_creation.rst index c1dd58a5de5..9be754fcd9d 100644 --- a/page_creation.rst +++ b/page_creation.rst @@ -21,7 +21,7 @@ two-step process: .. admonition:: Screencast :class: screencast - Do you prefer video tutorials? Check out the `Joyful Development with Symfony`_ + Do you prefer video tutorials? Check out the `Stellar Development with Symfony`_ screencast series. .. seealso:: @@ -42,22 +42,17 @@ Creating a Page: Route and Controller Suppose you want to create a page - ``/lucky/number`` - that generates a lucky (well, random) number and prints it. To do that, create a "Controller" class and a -"controller" method inside of it that will be executed when someone goes to -``/lucky/number``:: +"controller" method inside of it:: ` - in its own section, including how to make *variable* URLs; +#. *Create a route*: In ``config/routes.yaml``, the route defines the URL to your + page (``path``) and what ``controller`` to call. You'll learn more about :doc:`routing ` + in its own section, including how to make *variable* URLs; -#. *Create a controller*: The method below the route - ``numberAction()`` - is called - the *controller*. This is a function where *you* build the page and ultimately +#. *Create a controller*: This is a function where *you* build the page and ultimately return a ``Response`` object. You'll learn more about :doc:`controllers ` in their own section, including how to return JSON responses. +.. _annotation-routes: + +Annotation Routes +----------------- + +Instead of defining your route in YAML, Symfony also allows you to use *annotation* +routes. To do this, install the annotations package: + +.. code-block:: terminal + + $ composer require annotations + +You can now add your route directly *above* the controller: + +.. code-block:: diff + + // src/Controller/LuckyController.php + + // ... + + use Symfony\Component\Routing\Annotation\Route; + + class LuckyController + { + + /** + + * @Route("/lucky/number") + + */ + public function number() + { + // this looks exactly the same + } + } + +That's it! The page - ``http://localhost:8000/lucky/number`` will work exactly +like before! Annotations are the recommended way to configure routes. + +.. _flex-quick-intro: + +Auto-Installing Recipes with Symfony Flex +----------------------------------------- + +You may not have noticed, but when you ran ``composer require annotations``, two +special things happened, both thanks to a powerful Composer plugin called +:ref:`Flex `. + +First, ``annotations`` isn't a real package name: it's an *alias* (i.e. shortcut) +that Flex resolves to ``sensio/framework-extra-bundle``. + +Second, after this package was downloaded, Flex executed a *recipe*, which is a +set of automated instructions that tell Symfony how to integrate an external +package. `Flex recipes`_ exist for many packages and have the ability +to do a lot, like adding configuration files, creating directories, updating ``.gitignore`` +and adding new config to your ``.env`` file. Flex *automates* the installation of +packages so you can get back to coding. + +The bin/console Command +----------------------- + +Your project already has a powerful debugging tool inside: the ``bin/console`` command. +Try running it: + +.. code-block:: terminal + + $ php bin/console + +You should see a list of commands that can give you debugging information, help generate +code, generate database migrations and a lot more. As you install more packages, +you'll see more commands. + +To get a list of *all* of the routes in your system, use the ``debug:router`` command: + +.. code-block:: terminal + + $ php bin/console debug:router + +You should see your ``app_lucky_number`` route at the very top: + +================== ======== ======== ====== =============== + Name Method Scheme Host Path +================== ======== ======== ====== =============== + app_lucky_number ANY ANY ANY /lucky/number +================== ======== ======== ====== =============== + +You will also see debugging routes below ``app_lucky_number`` -- more on +the debugging routes in the next section. + +You'll learn about many more commands as you continue! + The Web Debug Toolbar: Debugging Dream -------------------------------------- -If your page is working, then you should *also* see a bar along the bottom of your -browser. This is called the Web Debug Toolbar: and it's your debugging best friend. -You'll learn more about all the information it holds along the way, but feel free -to experiment: hover over and click the different icons to get information about -routing, performance, logging and more. +One of Symfony's *killer* features is the Web Debug Toolbar: a bar that displays +a *huge* amount of debugging information along the bottom of your page while +developing. This is all included out of the box using a :ref:`Symfony pack ` +called ``symfony/profiler-pack``. -Rendering a Template (with the Service Container) -------------------------------------------------- +You will see a black bar along the bottom of the page. You'll learn more about all the information it holds +along the way, but feel free to experiment: hover over and click +the different icons to get information about routing, performance, logging and more. + +Rendering a Template +-------------------- If you're returning HTML from your controller, you'll probably want to render a template. Fortunately, Symfony comes with `Twig`_: a templating language that's easy, powerful and actually quite fun. -First, import the base :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` -class as shown on line 5 below. Then, let your ``LuckyController`` class -extend the base class:: +Install the twig package with: + +.. code-block:: terminal - // src/AppBundle/Controller/LuckyController.php + $ composer require twig + +Make sure that ``LuckyController`` extends Symfony's base +:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController` class: + +.. code-block:: diff + + // src/Controller/LuckyController.php // ... - // --> add this new use statement - use Symfony\Bundle\FrameworkBundle\Controller\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - class LuckyController extends Controller + - class LuckyController + + class LuckyController extends AbstractController { // ... } -Now, use the handy ``render()`` function to render a template. Pass it our ``number`` -variable so we can render that:: +Now, use the handy ``render()`` function to render a template. Pass it a ``number`` +variable so you can use it in Twig:: - // src/AppBundle/Controller/LuckyController.php + // src/Controller/LuckyController.php + namespace App\Controller; // ... - class LuckyController extends Controller + class LuckyController extends AbstractController { /** * @Route("/lucky/number") */ - public function numberAction() + public function number() { $number = random_int(0, 100); @@ -137,13 +241,13 @@ variable so we can render that:: } } -Finally, template files should live in the ``app/Resources/views`` directory. Create -a new ``app/Resources/views/lucky`` directory with a new ``number.html.twig`` file -inside: +Template files live in the ``templates/`` directory, which was created for you automatically +when you installed Twig. Create a new ``templates/lucky`` directory with a new +``number.html.twig`` file inside: .. code-block:: html+twig - {# app/Resources/views/lucky/number.html.twig #} + {# templates/lucky/number.html.twig #}

Your lucky number is {{ number }}

The ``{{ number }}`` syntax is used to *print* variables in Twig. Refresh your browser @@ -151,24 +255,31 @@ to get your *new* lucky number! http://localhost:8000/lucky/number -In the :doc:`/templating` article, you'll learn all about Twig: how to loop, render -other templates and leverage its powerful layout inheritance system. +Now you may wonder where the Web Debug Toolbar has gone: that's because there is +no ```` tag in the current template. You can add the body element yourself, +or extend ``base.html.twig``, which contains all default HTML elements. + +In the :doc:`templates ` article, you'll learn all about Twig: how +to loop, render other templates and leverage its powerful layout inheritance system. Checking out the Project Structure ---------------------------------- -Great news! You've already worked inside the two most important directories in your +Great news! You've already worked inside the most important directories in your project: -``app/`` - Contains things like configuration and templates. Basically, anything - that is *not* PHP code goes here. +``config/`` + Contains... configuration!. You will configure routes, + :doc:`services ` and packages. ``src/`` - Your PHP code lives here. + All your PHP code lives here. -99% of the time, you'll be working in ``src/`` (PHP files) or ``app/`` (everything -else). As you keep reading, you'll learn what can be done inside each of these. +``templates/`` + All your Twig templates live here. + +Most of the time, you'll be working in ``src/``, ``templates/`` or ``config/``. +As you keep reading, you'll learn what can be done inside each of these. So what about the other directories in the project? @@ -176,58 +287,20 @@ So what about the other directories in the project? The famous ``bin/console`` file lives here (and other, less important executable files). -``tests/`` - The automated tests (e.g. Unit tests) for your application live here. - ``var/`` This is where automatically-created files are stored, like cache files - (``var/cache/``), logs (``var/logs/``) and sessions (``var/sessions/``). + (``var/cache/``) and logs (``var/log/``). ``vendor/`` Third-party (i.e. "vendor") libraries live here! These are downloaded via the `Composer`_ package manager. -``web/`` - This is the document root for your project: put any publicly accessible files - here (e.g. CSS, JS and images). - -Bundles & Configuration ------------------------ - -Your Symfony application comes pre-installed with a collection of *bundles*, like -``FrameworkBundle`` and ``TwigBundle``. Bundles are similar to the idea of a *plugin*, -but with one important difference: *all* functionality in a Symfony application comes -from a bundle. - -Bundles are registered in your ``app/AppKernel.php`` file (a rare PHP file in the -``app/`` directory) and each gives you more *tools*, sometimes called *services*:: - - class AppKernel extends Kernel - { - public function registerBundles() - { - $bundles = [ - new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), - new Symfony\Bundle\TwigBundle\TwigBundle(), - // ... - ]; - // ... - - return $bundles; - } - - // ... - } - -For example, ``TwigBundle`` is responsible for adding the Twig tool to your app! - -Eventually, you'll download and add more third-party bundles to your app in order -to get even more tools. Imagine a bundle that helps you create paginated lists. -That exists! +``public/`` + This is the document root for your project: you put any publicly accessible files + here. -You can control how your bundles behave via the ``app/config/config.yml`` file. -That file - and other details like environments & parameters - are discussed in -the :doc:`/configuration` article. +And when you install new packages, new directories will be created automatically +when needed. What's Next? ------------ @@ -239,7 +312,7 @@ OK, time to finish mastering the fundamentals by reading these articles: * :doc:`/routing` * :doc:`/controller` -* :doc:`/templating` +* :doc:`/templates` * :doc:`/configuration` Then, learn about other important topics like the @@ -265,4 +338,5 @@ Go Deeper with HTTP & Framework Fundamentals .. _`Twig`: https://twig.symfony.com .. _`Composer`: https://getcomposer.org -.. _`Joyful Development with Symfony`: https://symfonycasts.com/screencast/symfony3 +.. _`Stellar Development with Symfony`: https://symfonycasts.com/screencast/symfony/setup +.. _`Flex recipes`: https://flex.symfony.com diff --git a/performance.rst b/performance.rst index 1feb0bd4025..51d2d83f230 100644 --- a/performance.rst +++ b/performance.rst @@ -12,13 +12,13 @@ Symfony Application Checklist ----------------------------- #. :ref:`Install APCu Polyfill if your server uses APC ` -#. :ref:`Enable APC Caching for the Autoloader ` -#. :ref:`Use Bootstrap Files ` +#. :ref:`Dump the service container into a single file ` Production Server Checklist --------------------------- #. :ref:`Use the OPcache byte code cache ` +#. :ref:`Use the OPcache class preloading ` #. :ref:`Configure OPcache for maximum performance ` #. :ref:`Don't check PHP files timestamps ` #. :ref:`Configure the PHP realpath Cache ` @@ -34,79 +34,45 @@ OPcache, install the `APCu Polyfill component`_ in your application to enable compatibility with `APCu PHP functions`_ and unlock support for advanced Symfony features, such as the APCu Cache adapter. -.. _performance-autoloader-apc-cache: +.. _performance-service-container-single-file: -Enable APC Caching for the Autoloader -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The class autoloading mechanism is one of the slowest parts in PHP applications -that make use of lots of classes, such as Symfony. A simple way to improve its -performance is to use the :class:`Symfony\\Component\\ClassLoader\\ApcClassLoader`, -which caches the location of each class after it's located the first time. - -To use it, adapt your front controller file like this:: - - // app.php - // ... - - $loader = require_once __DIR__.'/../app/bootstrap.php.cache'; - - // Change 'sf' by something unique to this app to prevent - // conflicts with other applications running in the same server - $loader = new ApcClassLoader('sf', $loader); - $loader->register(true); - - // ... - -For more details, see :doc:`/components/class_loader/cache_class_loader`. - -.. note:: - - 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. - -.. _performance-use-bootstrap-files: - -Use Bootstrap Files -~~~~~~~~~~~~~~~~~~~ - -.. caution:: +Dump the Service Container into a Single File +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Thanks to the optimizations introduced in PHP 7, bootstrap files are no - longer necessary when running your Symfony applications with PHP 7 or a - newer PHP version. +Symfony compiles the :doc:`service container ` into multiple +small files by default. Set this parameter to ``true`` to compile the entire +container into a single file, which could improve performance when using +"class preloading" in PHP 7.4 or newer versions: -The Symfony Standard Edition includes a script to generate a so-called -`bootstrap file`_, which is a large file containing the code of the most -commonly used classes. This saves a lot of IO operations because Symfony no -longer needs to look for and read those files. +.. configuration-block:: -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:: + .. code-block:: yaml - require_once __DIR__.'/../app/bootstrap.php.cache'; + # config/services.yaml + parameters: + # ... + container.dumper.inline_factories: true -Note that there are two disadvantages when using a bootstrap file: + .. code-block:: xml -* 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. + + + true + + -If you're using the Symfony Standard Edition, the bootstrap file is automatically -rebuilt after updating the vendor libraries via the ``composer install`` command. + .. code-block:: php -.. note:: + // config/services.php - 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. + // ... + $container->setParameter('container.dumper.inline_factories', true); .. _performance-use-opcache: @@ -118,6 +84,28 @@ every request. There are some `byte code caches`_ available, but as of PHP 5.5, PHP comes with `OPcache`_ built-in. For older versions, the most widely used byte code cache is `APC`_. +.. _performance-use-preloading: + +Use the OPcache class preloading +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Starting from PHP 7.4, OPcache can compile and load classes at start-up and +make them available to all requests until the server is restarted, improving +performance significantly. + +During container compilation, Symfony generates the file with the list of +classes to preload. The only requirement is that you need to set both +``container.dumper.inline_factories`` and ``container.dumper.inline_class_loader`` +parameters to ``true``. + +The preload file path is the same as the compiled service container but with the +``preload`` suffix: + +.. code-block:: ini + + ; php.ini + opcache.preload=/path/to/project/var/cache/prod/App_KernelProdContainer.preload.php + .. _performance-configure-opcache: Configure OPcache for Maximum Performance @@ -213,11 +201,9 @@ Learn more ---------- * :doc:`/http_cache/varnish` -* :doc:`/http_cache/form_csrf_caching` .. _`byte code caches`: https://en.wikipedia.org/wiki/List_of_PHP_accelerators .. _`OPcache`: https://php.net/manual/en/book.opcache.php -.. _`bootstrap file`: https://github.com/sensiolabs/SensioDistributionBundle/blob/master/Composer/ScriptHandler.php .. _`Composer's autoloader optimization`: https://getcomposer.org/doc/articles/autoloader-optimization.md .. _`APC`: https://php.net/manual/en/book.apc.php .. _`APCu Polyfill component`: https://github.com/symfony/polyfill-apcu diff --git a/profiler.rst b/profiler.rst index 496b86fbf62..6d9dafe60d8 100644 --- a/profiler.rst +++ b/profiler.rst @@ -1,8 +1,226 @@ Profiler ======== +The profiler is a powerful **development tool** that gives detailed information +about the execution of any request. **Never** enable the profiler in production +environments as it will lead to major security vulnerabilities in your project. + +Installation +------------ + +In applications using :ref:`Symfony Flex `, run this command to +install the ``profiler`` :ref:`Symfony pack ` before using it: + +.. code-block:: terminal + + $ composer require --dev symfony/profiler-pack + +Now browse any page of your application in the development environment to let +the profiler collect information. Then, click on any element of the debug +toolbar injected at the bottom of your pages to open the web interface of the +Symfony Profiler, which will look like this: + +.. image:: /_images/profiler/web-interface.png + :align: center + :class: with-browser + +Accessing Profiling Data Programmatically +----------------------------------------- + +Most of the times, the profiler information is accessed and analyzed using its +web-based interface. However, you can also retrieve profiling information +programmatically thanks to the methods provided by the ``profiler`` service. + +When the response object is available, use the +:method:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler::loadProfileFromResponse` +method to access to its associated profile:: + + // ... $profiler is the 'profiler' service + $profile = $profiler->loadProfileFromResponse($response); + +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. +Using this token, you can access the profile of any past response thanks to the +:method:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler::loadProfile` method:: + + $token = $response->headers->get('X-Debug-Token'); + $profile = $profiler->loadProfile($token); + +.. tip:: + + When the profiler is enabled but not the web debug toolbar, inspect the page + with your browser's developer tools to get the value of the ``X-Debug-Token`` + HTTP header. + +The ``profiler`` service also provides the +:method:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler::find` method to +look for tokens based on some criteria:: + + // gets the latest 10 tokens + $tokens = $profiler->find('', '', 10, '', '', ''); + + // gets the latest 10 tokens for all URL containing /admin/ + $tokens = $profiler->find('', '/admin/', 10, '', '', ''); + + // gets the latest 10 tokens for local POST requests + $tokens = $profiler->find('127.0.0.1', '', 10, 'POST', '', ''); + + // gets the latest 10 tokens for requests that happened between 2 and 4 days ago + $tokens = $profiler->find('', '', 10, '', '4 days ago', '2 days ago'); + +Data Collectors +--------------- + +The profiler gets its information using some services called "data collectors". +Symfony comes with several collectors that get information about the request, +the logger, the routing, the cache, etc. + +Run this command to get the list of collectors actually enabled in your app: + +.. code-block:: terminal + + $ php bin/console debug:container --tag=data_collector + +You can also :doc:`create your own data collector ` to +store any data generated by your app and display it in the debug toolbar and the +profiler web interface. + +.. _profiler-timing-execution: + +Timing the Execution of the Application +--------------------------------------- + +If you want to measure the time some tasks take in your application, there's no +need to create a custom data collector. Instead, use the `Stopwatch component`_ +which provides utilities to profile code and displays the results on the +"Performance" panel of the Profiler web interface. + +When using :ref:`autowiring `, type-hint any argument with +the :class:`Symfony\\Component\\Stopwatch\\Stopwatch` class and Symfony will +inject the Stopwatch service. Then, use the ``start()``, ``lap()`` and +``stop()`` methods to measure time:: + + // a user signs up and the timer starts... + $stopwatch->start('user-sign-up'); + + // ...do things to sign up the user... + $stopwatch->lap('user-sign-up'); + + // ...the sign up process is finished + $stopwatch->stop('user-sign-up'); + +.. tip:: + + Consider using a professional profiler such as `Blackfire`_ to measure and + analyze the execution of your application in detail. + +Enabling the Profiler Conditionally +----------------------------------- + +.. caution:: + + The possibility to use a matcher to enable the profiler conditionally was + removed in Symfony 4.0. + +Symfony Profiler cannot be enabled/disabled conditionally using matchers, because +that feature was removed in Symfony 4.0. However, you can use the ``enable()`` +and ``disable()`` methods of the :class:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler` +class in your controllers to manage the profiler programmatically:: + + use Symfony\Component\HttpKernel\Profiler\Profiler; + // ... + + class DefaultController + { + // ... + + public function someMethod(?Profiler $profiler) + { + // $profiler won't be set if your environment doesn't have the profiler (like prod, by default) + if (null !== $profiler) { + // if it exists, disable the profiler for this particular controller action + $profiler->disable(); + } + + // ... + } + } + +In order for the profiler to be injected into your controller you need to +create an alias pointing to the existing ``profiler`` service: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services_dev.yaml + services: + Symfony\Component\HttpKernel\Profiler\Profiler: '@profiler' + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/services_dev.php + use Symfony\Component\HttpKernel\Profiler\Profiler; + + $container->setAlias(Profiler::class, 'profiler'); + +Updating the Web Debug Toolbar After AJAX Requests +-------------------------------------------------- + +`Single-page applications`_ (SPA) are web applications that interact with the +user by dynamically rewriting the current page rather than loading entire new +pages from a server. + +By default, the debug toolbar displays the information of the initial page load +and doesn't refresh after each AJAX request. However, you can set the +``Symfony-Debug-Toolbar-Replace`` header to a value of ``1`` in the response to +the AJAX request to force the refresh of the toolbar:: + + $response->headers->set('Symfony-Debug-Toolbar-Replace', 1); + +Ideally this header should only be set during development and not for +production. To do that, create an :doc:`event subscriber ` +and listen to the :ref:`kernel.response` +event:: + + use Symfony\Component\HttpKernel\Event\ResponseEvent; + + // ... + + public function onKernelResponse(ResponseEvent $event) + { + if (!$this->getKernel()->isDebug()) { + return; + } + + $request = $event->getRequest(); + if (!$request->isXmlHttpRequest()) { + return; + } + + $response = $event->getResponse(); + $response->headers->set('Symfony-Debug-Toolbar-Replace', 1); + } + .. toctree:: - :maxdepth: 1 - :glob: + :hidden: + + profiler/data_collector - profiler/* +.. _`Single-page applications`: https://en.wikipedia.org/wiki/Single-page_application +.. _`Stopwatch component`: https://symfony.com/components/Stopwatch +.. _`Blackfire`: https://blackfire.io/docs/introduction?utm_source=symfony&utm_medium=symfonycom_docs&utm_campaign=profiler diff --git a/profiler/data_collector.rst b/profiler/data_collector.rst index 0898d4d81f9..55dd8fe7c31 100644 --- a/profiler/data_collector.rst +++ b/profiler/data_collector.rst @@ -21,8 +21,8 @@ property to store the collected information. The following example shows a custom collector that stores information about the request:: - // src/AppBundle/DataCollector/RequestCollector.php - namespace AppBundle\DataCollector; + // src/DataCollector/RequestCollector.php + namespace App\DataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -84,7 +84,7 @@ request:: Enabling Custom Data Collectors ------------------------------- -If you're using the :ref:`default services.yml configuration ` +If you're using the :ref:`default services.yaml configuration ` with ``autoconfigure``, then Symfony will automatically see your new data collector! Your ``collect()`` method should be called next time your refresh. @@ -101,8 +101,8 @@ template that includes some specific blocks. However, first you must add some getters in the data collector class to give the template access to the collected information:: - // src/AppBundle/DataCollector/RequestCollector.php - namespace AppBundle\DataCollector; + // src/DataCollector/RequestCollector.php + namespace App\DataCollector; use Symfony\Component\HttpKernel\DataCollector\DataCollector; @@ -132,8 +132,8 @@ block and set the value of two variables called ``icon`` and ``text``: {% block toolbar %} {% set icon %} {# this is the content displayed as a panel in the toolbar #} - - Request + ... + Request {% endset %} {% set text %} @@ -157,23 +157,15 @@ block and set the value of two variables called ``icon`` and ``text``: .. tip:: - Built-in collector templates define all their images as embedded base64-encoded - images. This makes them work everywhere without having to mess with web assets - links: - - .. code-block:: html - - - - Another solution is to define the images as SVG files. In addition to being - resolution-independent, these images can be embedded in the Twig template - or included from an external file to reuse them in several templates: + Built-in collector templates define all their images as embedded SVG files. + This makes them work everywhere without having to mess with web assets links: .. code-block:: twig - {{ include('data_collector/icon.svg') }} - - You are encouraged to use the latter technique for your own toolbar panels. + {% set icon %} + {{ include('data_collector/icon.svg') }} + {# ... #} + {% endset %} If the toolbar panel includes extended web profiler information, the Twig template must also define additional blocks: @@ -184,8 +176,7 @@ must also define additional blocks: {% block toolbar %} {% set icon %} - - Request + {# ... #} {% endset %} {% set text %} @@ -237,9 +228,9 @@ to specify a tag that contains the template: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: - AppBundle\DataCollector\RequestCollector: + App\DataCollector\RequestCollector: tags: - name: data_collector @@ -252,7 +243,7 @@ to specify a tag that contains the template: .. code-block:: xml - + - + autowire(RequestCollector::class) @@ -285,7 +276,7 @@ to specify a tag that contains the template: ]) ; -The position of each panel in the toolbar is determined by the priority defined -by each collector. Priorities are defined as positive or negative integers and -they default to ``0``. Most built-in collectors use ``255`` as their priority. -If you want your collector to be displayed before them, use a higher value (like 300). +The position of each panel in the toolbar is determined by the collector priority. +Priorities are defined as positive or negative integers and they default to ``0``. +Most built-in collectors use ``255`` as their priority. If you want your collector +to be displayed before them, use a higher value (like 300). diff --git a/profiler/matchers.rst b/profiler/matchers.rst deleted file mode 100644 index b929054d29a..00000000000 --- a/profiler/matchers.rst +++ /dev/null @@ -1,163 +0,0 @@ -.. index:: - single: Profiling; Matchers - -How to Use Matchers to Enable the Profiler Conditionally -======================================================== - -.. caution:: - - The possibility to use a matcher to enable the profiler conditionally is - deprecated since Symfony 3.4 and will be removed in 4.0. - -The Symfony profiler is only activated in the development environment to not hurt -your application performance. However, sometimes it may be useful to conditionally -enable the profiler in the production environment to assist you in debugging -issues. This behavior is implemented with the **Request Matchers**. - -Using the built-in Matcher --------------------------- - -A request matcher is a class that checks whether a given ``Request`` instance -matches a set of conditions. Symfony provides a -:class:`built-in matcher ` -which matches 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: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - # ... - profiler: - matcher: - ip: 168.0.0.1 - - .. code-block:: xml - - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', [ - // ... - 'profiler' => [ - 'matcher' => [ - '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 URLs which start with ``/admin/``. - -Creating a Custom Matcher -------------------------- - -Leveraging the concept of Request Matchers you can define a custom matcher to -enable the profiler conditionally in your application. To do so, create a class -which implements -:class:`Symfony\\Component\\HttpFoundation\\RequestMatcherInterface`. This -interface requires one method: -:method:`Symfony\\Component\\HttpFoundation\\RequestMatcherInterface::matches`. -This method returns ``false`` when the request doesn't match the conditions and -``true`` otherwise. Therefore, the custom matcher must return ``false`` to -disable the profiler and ``true`` to enable it. - -Suppose that the profiler must be enabled whenever a user with a -``ROLE_SUPER_ADMIN`` is logged in. This is the only code needed for that custom -matcher:: - - // src/AppBundle/Profiler/SuperAdminMatcher.php - namespace AppBundle\Profiler; - - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpFoundation\RequestMatcherInterface; - use Symfony\Component\Security\Core\Security; - - class SuperAdminMatcher implements RequestMatcherInterface - { - protected $security; - - public function __construct(Security $security) - { - $this->security = $security; - } - - public function matches(Request $request) - { - return $this->security->isGranted('ROLE_SUPER_ADMIN'); - } - } - -Then, you'll need to make sure your class is defined as a service. If you're using -the :ref:`default services.yml configuration `, -you don't need to do anything! - -Once the service is registered, the only thing left to do is configure the -profiler to use this service as the matcher: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - # ... - profiler: - matcher: - service: AppBundle\Profiler\SuperAdminMatcher - - .. code-block:: xml - - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - use AppBundle\Profiler\SuperAdminMatcher; - - $container->loadFromExtension('framework', [ - // ... - 'profiler' => [ - 'matcher' => [ - 'service' => SuperAdminMatcher::class, - ] - ], - ]); diff --git a/profiler/profiling_data.rst b/profiler/profiling_data.rst deleted file mode 100644 index bc59bc9580d..00000000000 --- a/profiler/profiling_data.rst +++ /dev/null @@ -1,47 +0,0 @@ -.. index:: - single: Profiling; Profiling data - -How to Access Profiling Data Programmatically -============================================= - -Most of the times, the profiler information is accessed and analyzed using its -web-based visualizer. However, you can also retrieve profiling information -programmatically thanks to the methods provided by the ``profiler`` service. - -When the response object is available, use the -:method:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler::loadProfileFromResponse` -method to access to its associated profile:: - - // ... $profiler is the 'profiler' service - $profile = $profiler->loadProfileFromResponse($response); - -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. -Using this token, you can access the profile of any past response thanks to the -:method:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler::loadProfile` method:: - - $token = $response->headers->get('X-Debug-Token'); - $profile = $container->get('profiler')->loadProfile($token); - -.. tip:: - - When the profiler is enabled but not the web debug toolbar, inspect the page - with your browser's developer tools to get the value of the ``X-Debug-Token`` - HTTP header. - -The ``profiler`` service also provides the -:method:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler::find` method to -look for tokens based on some criteria:: - - // gets the latest 10 tokens - $tokens = $container->get('profiler')->find('', '', 10, '', '', ''); - - // gets the latest 10 tokens for all URL containing /admin/ - $tokens = $container->get('profiler')->find('', '/admin/', 10, '', '', ''); - - // gets the latest 10 tokens for local POST requests - $tokens = $container->get('profiler')->find('127.0.0.1', '', 10, 'POST', '', ''); - - // gets 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'); diff --git a/profiler/storage.rst b/profiler/storage.rst deleted file mode 100644 index c4cb202a874..00000000000 --- a/profiler/storage.rst +++ /dev/null @@ -1,54 +0,0 @@ -.. index:: - single: Profiling; Storage Configuration - -Switching the Profiler Storage -============================== - -In Symfony versions prior to 3.0, profiles could be stored in files, databases, -services like Redis and memcache, etc. Starting from Symfony 3.0, the only storage -mechanism with built-in support is the filesystem. - -By default, the profile stores the collected data in the ``%kernel.cache_dir%/profiler/`` -directory. If you want to use another location to store the profiles, define the -``dsn`` option of the ``framework.profiler``: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - profiler: - dsn: 'file:/tmp/symfony/profiler' - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - - // ... - $container->loadFromExtension('framework', [ - 'profiler' => [ - 'dsn' => 'file:/tmp/symfony/profiler', - ], - ]); - -You can also create your own profile storage service implementing the -:class:`Symfony\\Component\\HttpKernel\\Profiler\\ProfilerStorageInterface` and -overriding the ``profiler.storage`` service. diff --git a/quick_tour/flex_recipes.rst b/quick_tour/flex_recipes.rst new file mode 100644 index 00000000000..dedd1e1083d --- /dev/null +++ b/quick_tour/flex_recipes.rst @@ -0,0 +1,266 @@ +Flex: Compose your Application +============================== + +After reading the first part of this tutorial, you have decided that Symfony was +worth another 10 minutes. Great choice! In this second part, you'll learn about +Symfony Flex: the amazing tool that makes adding new features as simple as running +one command. It's also the reason why Symfony is ideal for a small micro-service +or a huge application. Curious? Perfect! + +Symfony: Start Micro! +--------------------- + +Unless you're building a pure API (more on that soon!), you'll probably want to +render HTML. To do that, you'll use `Twig`_. Twig is a flexible, fast, and secure +template engine for PHP. It makes your templates more readable and concise; it also +makes them more friendly for web designers. + +Is Twig already installed in our application? Actually, not yet! And that's great! +When you start a new Symfony project, it's *small*: only the most critical dependencies +are included in your ``composer.json`` file: + +.. code-block:: text + + "require": { + "...", + "symfony/console": "^4.1", + "symfony/flex": "^1.0", + "symfony/framework-bundle": "^4.1", + "symfony/yaml": "^4.1" + } + +This makes Symfony different than any other PHP framework! Instead of starting with +a *bulky* app with *every* possible feature you might ever need, a Symfony app is +small, simple and *fast*. And you're in total control of what you add. + +Flex Recipes and Aliases +------------------------ + +So how can we install and configure Twig? By running one single command: + +.. code-block:: terminal + + $ composer require twig + +Two *very* interesting things happen behind the scenes thanks to Symfony Flex: a +Composer plugin that is already installed in our project. + +First, ``twig`` is not the name of a Composer package: it's a Flex *alias* that +points to ``symfony/twig-bundle``. Flex resolves that alias for Composer. + +And second, Flex installs a *recipe* for ``symfony/twig-bundle``. What's a recipe? +It's a way for a library to automatically configure itself by adding and modifying +files. Thanks to recipes, adding features is seamless and automated: install a package +and you're done! + +You can find a full list of recipes and aliases by going to `https://flex.symfony.com`_. + +What did this recipe do? In addition to automatically enabling the feature in +``config/bundles.php``, it added 3 things: + +``config/packages/twig.yaml`` + A configuration file that sets up Twig with sensible defaults. + +``config/routes/dev/twig.yaml`` + A route that helps you debug your error pages. + +``templates/`` + This is the directory where template files will live. The recipe also added + a ``base.html.twig`` layout file. + +Twig: Rendering a Template +-------------------------- + +Thanks to Flex, after one command, you can start using Twig immediately: + +.. code-block:: diff + + // src/Controller/DefaultController.php + namespace App\Controller; + + use Symfony\Component\Routing\Annotation\Route; + - use Symfony\Component\HttpFoundation\Response; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + + -class DefaultController + +class DefaultController extends AbstractController + { + /** + * @Route("/hello/{name}") + */ + public function index($name) + { + - return new Response("Hello $name!"); + + return $this->render('default/index.html.twig', [ + + 'name' => $name, + + ]); + } + } + +By extending ``AbstractController``, you now have access to a number of shortcut +methods and tools, like ``render()``. Create the new template: + +.. code-block:: html+twig + + {# templates/default/index.html.twig #} +

Hello {{ name }}

+ +That's it! The ``{{ name }}`` syntax will print the ``name`` variable that's passed +in from the controller. If you're new to Twig, welcome! You'll learn more about +its syntax and power later. + +But, right now, the page *only* contains the ``h1`` tag. To give it an HTML layout, +extend ``base.html.twig``: + +.. code-block:: html+twig + + {# templates/default/index.html.twig #} + {% extends 'base.html.twig' %} + + {% block body %} +

Hello {{ name }}

+ {% endblock %} + +This is called template inheritance: our page now inherits the HTML structure from +``base.html.twig``. + +Profiler: Debugging Paradise +---------------------------- + +One of the *coolest* features of Symfony isn't even installed yet! Let's fix that: + +.. code-block:: terminal + + $ composer require profiler + +Yes! This is another alias! And Flex *also* installs another recipe, which automates +the configuration of Symfony's Profiler. What's the result? Refresh! + +See that black bar on the bottom? That's the web debug toolbar, and it's your new +best friend. By hovering over each icon, you can get information about what controller +was executed, performance information, cache hits & misses and a lot more. Click +any icon to go into the *profiler* where you have even *more* detailed debugging +and performance data! + +Oh, and as you install more libraries, you'll get more tools (like a web debug toolbar +icon that shows database queries). + +You can now directly use the profiler because it configured *itself* thanks to +the recipe. What else can we install? + +Rich API Support +---------------- + +Are you building an API? You can already return JSON from any controller:: + + // src/Controller/DefaultController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class DefaultController extends AbstractController + { + // ... + + /** + * @Route("/api/hello/{name}") + */ + public function apiExample($name) + { + return $this->json([ + 'name' => $name, + 'symfony' => 'rocks', + ]); + } + } + +But for a *truly* rich API, try installing `API Platform`_: + +.. code-block:: terminal + + $ composer require api + +This is an alias to ``api-platform/api-pack`` :ref:`Symfony pack `, +which has dependencies on several other packages, like Symfony's Validator and +Security components, as well as the Doctrine ORM. In fact, Flex installed *5* recipes! + +But like usual, we can immediately start using the new library. Want to create a +rich API for a ``product`` table? Create a ``Product`` entity and give it the +``@ApiResource()`` annotation:: + + // src/Entity/Product.php + namespace App\Entity; + + use ApiPlatform\Core\Annotation\ApiResource; + use Doctrine\ORM\Mapping as ORM; + + /** + * @ORM\Entity() + * @ApiResource() + */ + class Product + { + /** + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + * @ORM\Column(type="integer") + */ + private $id; + + /** + * @ORM\Column(type="string") + */ + private $name; + + /** + * @ORM\Column(type="int") + */ + private $price; + + // ... + } + +Done! You now have endpoints to list, add, update and delete products! Don't believe +me? List your routes by running: + +.. code-block:: terminal + + $ php bin/console debug:router + + ------------------------------ -------- ------------------------------------- + Name Method Path + ------------------------------ -------- ------------------------------------- + api_products_get_collection GET /api/products.{_format} + api_products_post_collection POST /api/products.{_format} + api_products_get_item GET /api/products/{id}.{_format} + api_products_put_item PUT /api/products/{id}.{_format} + api_products_delete_item DELETE /api/products/{id}.{_format} + ... + ------------------------------ -------- ------------------------------------- + +.. _ easily-remove-recipes: + +Removing Recipes +---------------- + +Not convinced yet? No problem: remove the library: + +.. code-block:: terminal + + $ composer remove api + +Flex will *uninstall* the recipes: removing files and un-doing changes to put your +app back in its original state. Experiment without worry. + +More Features, Architecture and Speed +------------------------------------- + +I hope you're as excited about Flex as I am! But we still have *one* more chapter, +and it's the most important yet. I want to show you how Symfony empowers you to quickly +build features *without* sacrificing code quality or performance. It's all about +the service container, and it's Symfony's super power. Read on: about :doc:`/quick_tour/the_architecture`. + +.. _`https://flex.symfony.com`: https://flex.symfony.com +.. _`API Platform`: https://api-platform.com/ +.. _`Twig`: https://twig.symfony.com/ diff --git a/quick_tour/index.rst b/quick_tour/index.rst index 47972e911b3..6239a463be0 100644 --- a/quick_tour/index.rst +++ b/quick_tour/index.rst @@ -5,6 +5,5 @@ The Quick Tour :maxdepth: 1 the_big_picture - the_view - the_controller + flex_recipes the_architecture diff --git a/quick_tour/the_architecture.rst b/quick_tour/the_architecture.rst index d6d4cfd4be8..19d77648f7d 100644 --- a/quick_tour/the_architecture.rst +++ b/quick_tour/the_architecture.rst @@ -1,318 +1,345 @@ 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. -``bin/`` - Executable files (e.g. ``bin/console``). -``src/`` - The project's PHP code. -``tests/`` - Automatic tests (e.g. Unit tests). -``var/`` - Generated files (cache, logs, etc.). -``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 (the -file that handles all requests to your application) lives, such as the -production controller shown here:: - - // web/app.php - require_once __DIR__.'/../var/bootstrap.php.cache'; - require_once __DIR__.'/../app/AppKernel.php'; - - use Symfony\Component\HttpFoundation\Request; - - $kernel = new AppKernel('prod', false); - $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-dir: - -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 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() +You are my hero! Who would have thought that you would still be here after the first +two parts? Your efforts will be well-rewarded soon. The first two 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. + +Add Logging +----------- + +A new Symfony app is micro: it's basically just a routing & controller system. But +thanks to Flex, installing more features is simple. + +Want a logging system? No problem: + +.. code-block:: terminal + + $ composer require logger + +This installs and configures (via a recipe) the powerful `Monolog`_ library. To +use the logger in a controller, add a new argument type-hinted with ``LoggerInterface``:: + + // src/Controller/DefaultController.php + namespace App\Controller; + + use Psr\Log\LoggerInterface; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class DefaultController extends AbstractController { - $bundles = [ - 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(), ['dev', 'test'])) { - $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); - $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle(); - $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle(); + /** + * @Route("/hello/{name}") + */ + public function index($name, LoggerInterface $logger) + { + $logger->info("Saying hello to $name!"); + + // ... } - - 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: - -.. code-block:: yaml - - # app/config/config.yml - imports: - - { resource: parameters.yml } - - { resource: security.yml } - - { resource: services.yml } - - framework: - #esi: ~ - #translator: { fallbacks: ['%locale%'] } - secret: '%secret%' - router: - resource: '%kernel.project_dir%/app/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: - -.. code-block:: yaml - - # app/config/config_dev.yml - imports: - - { resource: config.yml } - - framework: - router: { resource: '%kernel.project_dir%/app/config/routing_dev.yml' } - profiler: { only_exceptions: false } - - web_profiler: - toolbar: true - intercept_redirects: false - - # ... - -Extending a Bundle -~~~~~~~~~~~~~~~~~~ - -In addition to being a nice way to organize and configure your code, a bundle -can extend another bundle. Bundle inheritance allows you to override any -existing bundle in order to customize its controllers, templates, or any -of its files. - -Logical File Names -.................. - -When you want to reference a file from a bundle, use this notation: -``@BUNDLE_NAME/path/to/file``; Symfony will resolve ``@BUNDLE_NAME`` -to the real path to the bundle. For instance, the logical path -``@AppBundle/Controller/DefaultController.php`` would be converted to -``src/AppBundle/Controller/DefaultController.php``, because Symfony knows -the location of the AppBundle. - -Logical Controller Names -........................ - -For controllers, you need to reference actions using the format -``BUNDLE_NAME:CONTROLLER_NAME:ACTION_NAME``. For instance, -``AppBundle:Default:index`` maps to the ``indexAction()`` method from the -``AppBundle\Controller\DefaultController`` class. - -Extending Bundles -................. - -If you follow these conventions, then you can use -:doc:`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: - -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 ``var/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: +That's it! The new log message will be written to ``var/log/dev.log``. The log +file path or even a different method of logging can be configured by updating +one of the config files added by the recipe. + +Services & Autowiring +--------------------- + +But wait! Something *very* cool just happened. Symfony read the ``LoggerInterface`` +type-hint and automatically figured out that it should pass us the Logger object! +This is called *autowiring*. + +Every bit of work that's done in a Symfony app is done by an *object*: the Logger +object logs things and the Twig object renders templates. These objects are called +*services* and they are *tools* that help you build rich features. + +To make life awesome, you can ask Symfony to pass you a service by using a type-hint. +What other possible classes or interfaces could you use? Find out by running: .. code-block:: terminal - $ php bin/console cache:clear --env=prod + $ php bin/console debug:autowiring -When developing a web application, things can go wrong in many ways. The -log files in the ``var/logs/`` directory tell you everything about the requests -and help you fix the problem quickly. + # this is just a *small* sample of the output... -Using the Command Line Interface --------------------------------- + Describes a logger instance. + Psr\Log\LoggerInterface (monolog.logger) -Each application comes with a command line interface tool (``bin/console``) -that helps you maintain your application. It provides commands that boost -your productivity by automating tedious and repetitive tasks. + Request stack that controls the lifecycle of requests. + Symfony\Component\HttpFoundation\RequestStack (request_stack) -Run it without any arguments to learn more about its capabilities: + Interface for the session. + Symfony\Component\HttpFoundation\Session\SessionInterface (session) -.. code-block:: terminal + RouterInterface is the interface that all Router classes must implement. + Symfony\Component\Routing\RouterInterface (router.default) + + [...] + +This is just a short summary of the full list! And as you add more packages, this +list of tools will grow! + +Creating Services +----------------- + +To keep your code organized, you can even create your own services! Suppose you +want to generate a random greeting (e.g. "Hello", "Yo", etc). Instead of putting +this code directly in your controller, create a new class:: + + // src/GreetingGenerator.php + namespace App; + + class GreetingGenerator + { + public function getRandomGreeting() + { + $greetings = ['Hey', 'Yo', 'Aloha']; + $greeting = $greetings[array_rand($greetings)]; + + return $greeting; + } + } + +Great! You can use this immediately in your controller:: + + // src/Controller/DefaultController.php + namespace App\Controller; + + use App\GreetingGenerator; + use Psr\Log\LoggerInterface; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class DefaultController extends AbstractController + { + /** + * @Route("/hello/{name}") + */ + public function index($name, LoggerInterface $logger, GreetingGenerator $generator) + { + $greeting = $generator->getRandomGreeting(); + + $logger->info("Saying $greeting to $name!"); + + // ... + } + } + +That's it! Symfony will instantiate the ``GreetingGenerator`` automatically and +pass it as an argument. But, could we *also* move the logger logic to ``GreetingGenerator``? +Yes! You can use autowiring inside a service to access *other* services. The only +difference is that it's done in the constructor: + +.. code-block:: diff + + // src/GreetingGenerator.php + + use Psr\Log\LoggerInterface; + + class GreetingGenerator + { + + private $logger; + + + + public function __construct(LoggerInterface $logger) + + { + + $this->logger = $logger; + + } + + public function getRandomGreeting() + { + // ... + + + $this->logger->info('Using the greeting: '.$greeting); + + return $greeting; + } + } + +Yes! This works too: no configuration, no time wasted. Keep coding! + +Twig Extension & Autoconfiguration +---------------------------------- + +Thanks to Symfony's service handling, you can *extend* Symfony in many ways, like +by creating an event subscriber or a security voter for complex authorization +rules. Let's add a new filter to Twig called ``greet``. How? Create a class +that extends ``AbstractExtension``:: + + // src/Twig/GreetExtension.php + namespace App\Twig; + + use App\GreetingGenerator; + use Twig\Extension\AbstractExtension; + use Twig\TwigFilter; + + class GreetExtension extends AbstractExtension + { + private $greetingGenerator; + + public function __construct(GreetingGenerator $greetingGenerator) + { + $this->greetingGenerator = $greetingGenerator; + } + + public function getFilters() + { + return [ + new TwigFilter('greet', [$this, 'greetUser']), + ]; + } + + public function greetUser($name) + { + $greeting = $this->greetingGenerator->getRandomGreeting(); + + return "$greeting $name!"; + } + } + +After creating just *one* file, you can use this immediately: + +.. code-block:: html+twig + + {# templates/default/index.html.twig #} + {# Will print something like "Hey Symfony!" #} +

{{ name|greet }}

+ +How does this work? Symfony notices that your class extends ``AbstractExtension`` +and so *automatically* registers it as a Twig extension. This is called autoconfiguration, +and it works for *many* many things. Create a class and then extend a base class +(or implement an interface). Symfony takes care of the rest. + +Blazing Speed: The Cached Container +----------------------------------- + +After seeing how much Symfony handles automatically, you might be wondering: "Doesn't +this hurt performance?" Actually, no! Symfony is blazing fast. + +How is that possible? The service system is managed by a very important object called +the "container". Most frameworks have a container, but Symfony's is unique because +it's *cached*. When you loaded your first page, all of the service information was +compiled and saved. This means that the autowiring and autoconfiguration features +add *no* overhead! It also means that you get *great* errors: Symfony inspects and +validates *everything* when the container is built. - $ php bin/console +Now you might be wondering what happens when you update a file and the cache needs +to rebuild? I like your thinking! It's smart enough to rebuild on the next page +load. But that's really the topic of the next section. -The ``--help`` option helps you discover the usage of a command: +Development Versus Production: Environments +------------------------------------------- + +One of a framework's main jobs is to make debugging easy! And our app is *full* of +great tools for this: the web debug toolbar displays at the bottom of the page, errors +are big, beautiful & explicit, and any configuration cache is automatically rebuilt +whenever needed. + +But what about when you deploy to production? We will need to hide those tools and +optimize for speed! + +This is solved by Symfony's *environment* system and there are three: ``dev``, ``prod`` +and ``test``. Based on the environment, Symfony loads different files in the ``config/`` +directory: + +.. code-block:: text + + config/ + ├─ services.yaml + ├─ ... + └─ packages/ + ├─ framework.yaml + ├─ ... + ├─ **dev/** + ├─ monolog.yaml + └─ ... + ├─ **prod/** + └─ monolog.yaml + └─ **test/** + ├─ framework.yaml + └─ ... + └─ routes/ + ├─ annotations.yaml + └─ **dev/** + ├─ twig.yaml + └─ web_profiler.yaml + +This is a *powerful* idea: by changing one piece of configuration (the environment), +your app is transformed from a debugging-friendly experience to one that's optimized +for speed. + +Oh, how do you change the environment? Change the ``APP_ENV`` environment variable +from ``dev`` to ``prod``: + +.. code-block:: diff + + # .env + - APP_ENV=dev + + APP_ENV=prod + +But I want to talk more about environment variables next. Change the value back +to ``dev``: debugging tools are great when you're working locally. + +Environment Variables +--------------------- + +Every app contains configuration that's different on each server - like database +connection information or passwords. How should these be stored? In files? Or some +other way? + +Symfony follows the industry best practice by storing server-based configuration +as *environment* variables. This means that Symfony works *perfectly* with +Platform as a Service (PaaS) deployment systems as well as Docker. + +But setting environment variables while developing can be a pain. That's why your +app automatically loads a ``.env`` file. The keys in this file then become environment +variables and are read by your app: + +.. code-block:: bash + + # .env + ###> symfony/framework-bundle ### + APP_ENV=dev + APP_SECRET=cc86c7ca937636d5ddf1b754beb22a10 + ###< symfony/framework-bundle ### + +At first, the file doesn't contain much. But as your app grows, you'll add more +configuration as you need it. But, actually, it gets much more interesting! Suppose +your app needs a database ORM. Let's install the Doctrine ORM: .. code-block:: terminal - $ php bin/console debug:router --help + $ composer require doctrine + +Thanks to a new recipe installed by Flex, look at the ``.env`` file again: + +.. code-block:: diff + + ###> symfony/framework-bundle ### + APP_ENV=dev + APP_SECRET=cc86c7ca937636d5ddf1b754beb22a10 + ###< symfony/framework-bundle ### + + + ###> doctrine/doctrine-bundle ### + + # ... + + DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name + + ###< doctrine/doctrine-bundle ### + +The new ``DATABASE_URL`` environment variable was added *automatically* and is already +referenced by the new ``doctrine.yaml`` configuration file. By combining environment +variables and Flex, you're using industry best practices without any extra effort. -Final Thoughts --------------- +Keep Going! +----------- -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. +Call me crazy, but after reading this part, you should be comfortable with the most +*important* parts of Symfony. Everything in Symfony is designed to get out of your +way so you can keep coding and adding features, all with the speed and quality you +demand. -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 :doc:`/index` and -pick any topic you want. +That's all for the quick tour. From authentication, to forms, to caching, there is +so much more to discover. Ready to dig into these topics now? Look no further - go +to the official :doc:`/index` and pick any guide you want. -.. _`Composer`: https://getcomposer.org +.. _`Monolog`: https://github.com/Seldaek/monolog diff --git a/quick_tour/the_big_picture.rst b/quick_tour/the_big_picture.rst index 1a75af1a533..7ec7f08ac51 100644 --- a/quick_tour/the_big_picture.rst +++ b/quick_tour/the_big_picture.rst @@ -1,261 +1,200 @@ 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. +Start using Symfony in 10 minutes! Really! That's all you need to understand the +most important concepts and start building a real project! 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. +Symfony. If not, welcome to a whole new way of developing web applications. Symfony +*embraces* best practices, keeps backwards compatibility (Yes! Upgrading is always +safe & easy!) and offers long-term support. .. _installing-symfony2: -Installing Symfony ------------------- +Downloading Symfony +------------------- -Before continuing reading this chapter, make sure to have installed both PHP -and Symfony as explained in the :doc:`/setup` article. +First, make sure you've installed `Composer`_ and have PHP 7.1.3 or higher. -Understanding the Fundamentals ------------------------------- +Ready? In a terminal, run: -One of the main goals of a framework is to keep your code organized and -to allow your application to evolve 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. +.. code-block:: terminal -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 ``Homepage`` HTML page). + $ composer create-project symfony/skeleton quick_tour -The code to execute is defined as methods of PHP classes. The methods are -called **actions** and the classes **controllers**, but in practice most -developers use **controllers** to refer to both of them. 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**. +This creates a new ``quick_tour/`` directory with a small, but powerful new +Symfony application: -When you go to ``http://localhost:8000/app/example``, Symfony will execute -the controller in ``src/AppBundle/Controller/DefaultController.php`` and -render the ``app/Resources/views/default/index.html.twig`` template. +.. code-block:: text -In the following sections you'll learn in detail the inner workings of Symfony -controllers, routes and templates. + quick_tour/ + ├─ .env + ├─ bin/console + ├─ composer.json + ├─ composer.lock + ├─ config/ + ├─ public/index.php + ├─ src/ + ├─ symfony.lock + ├─ var/ + └─ vendor/ -Actions and Controllers -~~~~~~~~~~~~~~~~~~~~~~~ +Can we already load the project in a browser? Yes! You can setup +:doc:`Nginx or Apache ` and configure their +document root to be the ``public/`` directory. But, for development, it's better +to :doc:`install the Symfony local web server ` and run +it as follows: -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):: +.. code-block:: terminal - namespace AppBundle\Controller; + $ symfony server:start - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Routing\Annotation\Route; +Try your new app by going to ``http://localhost:8000`` in a browser! - class DefaultController extends Controller - { - /** - * @Route("/", name="homepage") - */ - public function indexAction() - { - return $this->render('default/index.html.twig', [ - // ... - ]); - } - } +.. image:: /_images/quick_tour/no_routes_page.png + :align: center + :class: with-browser -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``. +Fundamentals: Route, Controller, Response +----------------------------------------- -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. +Our project only has about 15 files, but it's ready to become a sleek API, a robust +web app, or a microservice. Symfony starts small, but scales with you. -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. +But before we go too far, let's dig into the fundamentals by building our first page. -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 to welcome -you to Symfony. +Start in ``config/routes.yaml``: this is where *we* can define the URL to our new +page. Uncomment the example that already lives in the file: -Routing -~~~~~~~ +.. code-block:: yaml -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:: + # config/routes.yaml + index: + path: / + controller: 'App\Controller\DefaultController::index' - // src/AppBundle/Controller/DefaultController.php - namespace AppBundle\Controller; +This is called a *route*: it defines the URL to your page (``/``) and the "controller": +the *function* that will be called whenever anyone goes to this URL. That function +doesn't exist yet, so let's create it! - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Routing\Annotation\Route; +In ``src/Controller``, create a new ``DefaultController`` class and an ``index`` +method inside:: - class DefaultController extends Controller + // src/Controller/DefaultController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + + class DefaultController { - /** - * @Route("/", name="homepage") - */ - public function indexAction() + public function index() { - return $this->render('default/index.html.twig', [ - // ... - ]); + return new Response('Hello!'); } } -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. - -.. tip:: - - In addition to PHP annotations, routes can be configured in YAML, XML - or PHP files, as explained in the :doc:`/routing` guide. 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', [ - // ... - ]); +That's it! Try going to the homepage: ``http://localhost:8000/``. Symfony sees +that the URL matches our route and then executes the new ``index()`` method. -The ``$this->render()`` method is a convenient shortcut to render a template. -Symfony provides some useful shortcuts to any controller extending from -the base Symfony :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` -class. +A controller is just a normal function with *one* rule: it must return a Symfony +``Response`` object. But that response can contain anything: simple text, JSON or +a full HTML page. -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: +But the routing system is *much* more powerful. So let's make the route more interesting: -.. code-block:: html+twig +.. code-block:: diff - {# app/Resources/views/default/index.html.twig #} - {% extends 'base.html.twig' %} + # config/routes.yaml + index: + - path: / + + path: /hello/{name} + controller: 'App\Controller\DefaultController::index' - {% block body %} -

Welcome to Symfony

+The URL to this page has changed: it is *now* ``/hello/*``: the ``{name}`` acts +like a wildcard that matches anything. And it gets better! Update the controller too: - {# ... #} - {% endblock %} +.. code-block:: diff -This template is created with `Twig`_, a template engine created for modern PHP -applications. The :doc:`second part of this tutorial ` -explains how templates work in Symfony. + // src/Controller/DefaultController.php + namespace App\Controller; -.. _quick-tour-big-picture-environments: + use Symfony\Component\HttpFoundation\Response; -Working with Environments -------------------------- + class DefaultController + { + - public function index() + + public function index($name) + { + - return new Response('Hello!'); + + return new Response("Hello $name!"); + } + } -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! +Try the page out by going to ``http://localhost:8000/hello/Symfony``. You should +see: Hello Symfony! The value of the ``{name}`` in the URL is available as a ``$name`` +argument in your controller. -.. image:: /_images/quick_tour/web_debug_toolbar.png - :align: center - :class: with-browser +But this can be even simpler! So let's install annotations support: -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: +.. code-block:: terminal -.. image:: /_images/quick_tour/profiler.png - :align: center - :class: with-browser + $ composer require annotations -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. +Now, comment-out the YAML route by adding the ``#`` character: -How does Symfony know whether your application is running locally or on -a production server? Keep reading to discover the concept of **execution -environments**. - -.. _quick-tour-big-picture-environments-intro: +.. code-block:: yaml -What is an Environment? -~~~~~~~~~~~~~~~~~~~~~~~ + # config/routes.yaml + # index: + # path: /hello/{name} + # controller: 'App\Controller\DefaultController::index' -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). +Instead, add the route *right above* the controller method: -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. +.. code-block:: diff -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. + // src/Controller/DefaultController.php + namespace App\Controller; -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. + use Symfony\Component\HttpFoundation\Response; + + use Symfony\Component\Routing\Annotation\Route; -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: + class DefaultController + { + + /** + + * @Route("/hello/{name}") + + */ + public function index($name) { + // ... + } + } -.. code-block:: yaml +This works just like before! But by using annotations, the route and controller +live right next to each other. Need another page? Add another route and method +in ``DefaultController``:: - # app/config/config_dev.yml - imports: - - { resource: config.yml } + // src/Controller/DefaultController.php + namespace App\Controller; - web_profiler: - toolbar: true - intercept_redirects: false + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Annotation\Route; -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. + class DefaultController + { + // ... -For more details on environments, see -:ref:`the "Environments" section ` of the -Configuration guide. + /** + * @Route("/simplicity") + */ + public function simple() + { + return new Response('Simple! Easy! Great!'); + } + } -Final Thoughts --------------- +Routing can do *even* more, but we'll save that for another time! Right now, our +app needs more features! Like a template engine, logging, debugging tools and more. -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: -":doc:`The View `". +Keep reading with :doc:`/quick_tour/flex_recipes`. -.. _`Twig`: https://twig.symfony.com/ +.. _`Composer`: https://getcomposer.org/ diff --git a/quick_tour/the_controller.rst b/quick_tour/the_controller.rst deleted file mode 100644 index 2c4b94609d9..00000000000 --- a/quick_tour/the_controller.rst +++ /dev/null @@ -1,348 +0,0 @@ -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()`` -controller shortcut method to return a rendered template 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 Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; - - 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 Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Routing\Annotation\Route; - - class DefaultController extends Controller - { - // ... - - /** - * @Route("/hello/{name}", name="hello") - */ - public function helloAction($name) - { - return $this->render('default/hello.html.twig', [ - '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: - -.. code-block:: html+twig - - {# app/Resources/views/default/hello.html.twig #} - {% extends 'base.html.twig' %} - - {% block body %} -

Hi {{ name }}! Welcome to Symfony!

- {% 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 Symfony\Component\Routing\Annotation\Route; - - // ... - - /** - * @Route("/hello/{name}.{_format}", defaults={"_format"="html"}, name="hello") - */ - public function helloAction($name, $_format) - { - return $this->render('default/hello.'.$_format.'.twig', [ - 'name' => $name, - ]); - } - -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: - -.. code-block:: xml+php - - - - {{ name }} - - -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 Symfony\Component\Routing\Annotation\Route; - - // ... - - /** - * @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', [ - '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: - -Redirecting ------------ - -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', ['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. - -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 Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Routing\Annotation\Route; - - 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(['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: - -.. code-block:: twig - - {{ 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 achieved -from any controller:: - - use Symfony\Component\HttpFoundation\Session\Session; - - public function indexAction(Session $session) - { - // stores an attribute for reuse during a later user request - $session->set('foo', 'bar'); - - // gets the value of a session attribute - $foo = $session->get('foo'); - - // uses 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() - { - // ... - - // 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: - -.. code-block:: html+twig - - {% for message in app.flashes('notice') %} -
- {{ message }} -
- {% endfor %} - -.. versionadded:: 3.3 - - The ``app.flashes()`` Twig function was introduced in Symfony 3.3. Prior, - you had to use ``app.session.flashBag()``. - -Final Thoughts --------------- - -That's all there is to it and I'm not even sure you 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 FrameworkBundle. -But thanks to bundles, everything in Symfony can be extended or replaced. -That's the topic of the :doc:`next part of this tutorial `. diff --git a/quick_tour/the_view.rst b/quick_tour/the_view.rst deleted file mode 100644 index 3f48ff2607e..00000000000 --- a/quick_tour/the_view.rst +++ /dev/null @@ -1,291 +0,0 @@ -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: - -.. code-block:: html+twig - - - - - {{ page_title }} - - -

{{ page_title }}

- - - - - -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', [ - '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: - -.. code-block:: twig - - {# 1. Simple variables #} - {# $this->render('template.html.twig', [ - 'name' => 'Fabien', - ]) #} - {{ name }} - - {# 2. Arrays #} - {# $this->render('template.html.twig', [ - 'user' => ['name' => 'Fabien'], - ]) #} - {{ user.name }} - - {# alternative syntax for arrays #} - {{ user['name'] }} - - {# 3. Objects #} - {# $this->render('template.html.twig', [ - '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: - -.. code-block:: html+twig - - {# app/Resources/views/default/index.html.twig #} - {% extends 'base.html.twig' %} - - {% block body %} -

Welcome to Symfony!

- {% 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: - -.. code-block:: html+twig - - {# app/Resources/views/base.html.twig #} - - - - - {% block title %}Welcome!{% endblock %} - {% block stylesheets %}{% endblock %} - - - - {% block body %}{% endblock %} - {% block javascripts %}{% endblock %} - - - -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: - -.. code-block:: html+twig - -

{{ article.title|capitalize }}

- -

{{ article.content|striptags|slice(0, 255) }} ...

- -

Tags: {{ article.tags|sort|join(", ") }}

- -

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

- -Don't forget to check out the official `Twig documentation`_ to learn everything -about filters, functions and tags. - -.. _including-other-templates: - -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: - -.. code-block:: html+twig - - {# app/Resources/views/ads/banner.html.twig #} -
- ... -
- -To display this ad on any page, include the ``banner.html.twig`` template -using the ``include()`` function: - -.. code-block:: html+twig - - {# app/Resources/views/default/index.html.twig #} - {% extends 'base.html.twig' %} - - {% block body %} -

Welcome to Symfony!

- - {{ 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: - -.. code-block:: twig - - {# 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', [ - '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 updated by just changing the configuration: - -.. code-block:: html+twig - - Return to homepage - -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. - -.. tip:: - - The ``url()`` function is very similar to the ``path()`` function, but generates - *absolute* URLs, which is very handy when rendering emails and RSS files: - ``Visit our website``. - -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: - -.. code-block:: html+twig - - - - - -The ``asset()`` function looks for the web assets inside the ``web/`` directory. -If you store them in another directory, read -:doc:`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 :doc:`next part of this tutorial -`. Ready for another 10 minutes with Symfony? - -.. _`Twig`: https://twig.symfony.com/ -.. _`Twig documentation`: https://twig.symfony.com/doc/2.x/ diff --git a/reference/configuration/assetic.rst b/reference/configuration/assetic.rst deleted file mode 100644 index 4ab48915c01..00000000000 --- a/reference/configuration/assetic.rst +++ /dev/null @@ -1,17 +0,0 @@ -.. index:: - pair: Assetic; Configuration reference - -Assetic Configuration Reference (AsseticBundle) -=============================================== - -.. include:: /assetic/_standard_edition_warning.rst.inc - -You can get the full Assetic configuration reference as follows: - -.. code-block:: terminal - - # displays the default config values defined by Symfony - $ php bin/console config:dump-reference assetic - - # displays the actual config values used by your application - $ php bin/console debug:config assetic diff --git a/reference/configuration/debug.rst b/reference/configuration/debug.rst index bf23d951ea9..6732459f3c3 100644 --- a/reference/configuration/debug.rst +++ b/reference/configuration/debug.rst @@ -32,20 +32,16 @@ Configuration * `min_depth`_ * `max_string_length`_ -``max_items`` -~~~~~~~~~~~~~ +max_items +~~~~~~~~~ **type**: ``integer`` **default**: ``2500`` This is the maximum number of items to dump. Setting this option to ``-1`` disables the limit. -``min_depth`` -~~~~~~~~~~~~~ - -.. versionadded:: 3.4 - - The ``min_depth`` option was introduced in Symfony 3.4. +min_depth +~~~~~~~~~ **type**: ``integer`` **default**: ``1`` @@ -54,16 +50,18 @@ be cloned. After this depth is reached, only ``max_items`` items will be cloned. The default value is ``1``, which is consistent with older Symfony versions. -``max_string_length`` -~~~~~~~~~~~~~~~~~~~~~ +max_string_length +~~~~~~~~~~~~~~~~~ **type**: ``integer`` **default**: ``-1`` This option configures the maximum string length before truncating the string. The default value (``-1``) means that strings are never truncated. -``dump_destination`` -~~~~~~~~~~~~~~~~~~~~ +.. _configuration-debug-dump_destination: + +dump_destination +~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``null`` @@ -77,13 +75,13 @@ destination for dumps. Typically, you would set this to ``php://stderr``: .. code-block:: yaml - # app/config/config.yml + # config/packages/debug.yaml debug: dump_destination: php://stderr .. code-block:: xml - + loadFromExtension('debug', [ 'dump_destination' => 'php://stderr', ]); + +Configure it to ``"tcp://%env(VAR_DUMPER_SERVER)%"`` in order to use the :ref:`ServerDumper feature `. diff --git a/reference/configuration/doctrine.rst b/reference/configuration/doctrine.rst index 8f9d402683d..6117577ee8f 100644 --- a/reference/configuration/doctrine.rst +++ b/reference/configuration/doctrine.rst @@ -53,23 +53,23 @@ The following block shows all possible configuration keys: # if the url option is specified, it will override the above config url: mysql://db_user:db_password@127.0.0.1:3306/db_name # the DBAL driverClass option - driver_class: MyNamespace\MyDriverImpl + driver_class: App\DBAL\MyDatabaseDriver # the DBAL driverOptions option options: foo: bar - path: '%kernel.project_dir%/app/data/data.sqlite' + path: '%kernel.project_dir%/var/data/data.sqlite' memory: true unix_socket: /tmp/mysql.sock # the DBAL wrapperClass option - wrapper_class: MyDoctrineDbalConnectionWrapper + wrapper_class: App\DBAL\MyConnectionWrapper charset: UTF8 logging: '%kernel.debug%' - platform_service: MyOwnDatabasePlatformService + platform_service: App\DBAL\MyDatabasePlatformService server_version: '5.6' mapping_types: enum: string types: - custom: Acme\HelloBundle\MyCustomType + custom: App\DBAL\MyCustomType .. code-block:: xml @@ -91,19 +91,19 @@ The following block shows all possible configuration keys: user="user" password="secret" driver="pdo_mysql" - driver-class="MyNamespace\MyDriverImpl" + driver-class="App\DBAL\MyDatabaseDriver" path="%kernel.project_dir%/var/data/data.sqlite" memory="true" unix-socket="/tmp/mysql.sock" - wrapper-class="MyDoctrineDbalConnectionWrapper" + wrapper-class="App\DBAL\MyConnectionWrapper" charset="UTF8" logging="%kernel.debug%" - platform-service="MyOwnDatabasePlatformService" + platform-service="App\DBAL\MyDatabasePlatformService" server-version="5.6"> bar string - Acme\HelloBundle\MyCustomType + App\DBAL\MyCustomType @@ -155,7 +155,13 @@ 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. +service where ``[name]`` is the name of the connection. In a controller +extending ``AbstractController``, you can access it directly using the +``getConnection()`` method and the name of the connection:: + + $connection = $this->getDoctrine()->getConnection('customer'); + + $result = $connection->fetchAll('SELECT name FROM customer'); Doctrine ORM Configuration -------------------------- @@ -238,7 +244,7 @@ The following example shows an overview of the caching configurations: # the 'service' type requires to define the 'id' option too query_cache_driver: type: service - id: my_doctrine_common_cache_service + id: App\ORM\MyCacheService Mapping Configuration ~~~~~~~~~~~~~~~~~~~~~ @@ -250,42 +256,33 @@ you can control. The following configuration options exist for a mapping: ``type`` ........ -One of ``annotation``, ``xml``, ``yml``, ``php`` or ``staticphp``. This -specifies which type of metadata type your mapping uses. +One of ``annotation`` (the default value), ``xml``, ``yml``, ``php`` or +``staticphp``. This specifies which type of metadata type your mapping uses. ``dir`` ....... -Path to the mapping or entity files (depending on the driver). If this path -is relative, it is assumed to be relative to the bundle root. This only works -if the name of your mapping is a bundle name. If you want to use this option -to specify absolute paths you should prefix the path with the kernel parameters -that exist in the DIC (for example ``%kernel.project_dir%``). +Absolute path to the mapping or entity files (depending on the driver). ``prefix`` .......... -A common namespace prefix that all entities of this mapping share. This -prefix should never conflict with prefixes of other defined mappings otherwise -some of your entities cannot be found by Doctrine. This option defaults -to the bundle namespace + ``Entity``, for example for an application bundle -called AcmeHelloBundle prefix would be ``Acme\HelloBundle\Entity``. +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. ``alias`` ......... Doctrine offers a way to alias entity namespaces to simpler, shorter names -to be used in DQL queries or for Repository access. When using a bundle -the alias defaults to the bundle name. +to be used in DQL queries or for Repository access. ``is_bundle`` ............. -This option is a derived value from ``dir`` and by default is set to ``true`` -if dir is relative proved by a ``file_exists()`` check that returns ``false``. -It is ``false`` if the existence check returns ``true``. In this case an -absolute path was specified and the metadata files are most likely in a -directory outside of a bundle. +This option is ``false`` by default and it's considered a legacy option. It was +only useful in previous Symfony versions, when it was recommended to use bundles +to organize the application code. Custom Mapping Entities in a Bundle ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -351,9 +348,7 @@ directory instead: Mapping Entities Outside of a Bundle ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can also create new mappings, for example outside of the Symfony folder. - -For example, the following looks for entity classes in the ``App\Entity`` +For example, the following looks for entity classes in the ``Entity`` namespace in the ``src/Entity`` directory and gives them an ``App`` alias (so you can say things like ``App:Post``): @@ -420,7 +415,7 @@ If the ``type`` on the bundle configuration isn't set, the DoctrineBundle will try to detect the correct mapping configuration format for the bundle. DoctrineBundle will look for files matching ``*.orm.[FORMAT]`` (e.g. -``Post.orm.yml``) in the configured ``dir`` of your mapping (if you're mapping +``Post.orm.yaml``) in the configured ``dir`` of your mapping (if you're mapping a bundle, then ``dir`` is relative to the bundle's directory). The bundle looks for (in this order) XML, YAML and PHP files. diff --git a/reference/configuration/framework.rst b/reference/configuration/framework.rst index 8c2158d173a..11672d517d6 100644 --- a/reference/configuration/framework.rst +++ b/reference/configuration/framework.rst @@ -50,6 +50,7 @@ Configuration * :ref:`app ` * `default_doctrine_provider`_ * `default_memcached_provider`_ + * `default_pdo_provider`_ * `default_psr6_provider`_ * `default_redis_provider`_ * `directory`_ @@ -62,6 +63,7 @@ Configuration * `default_lifetime`_ * `provider`_ * `public`_ + * `tags`_ * `prefix_seed`_ * `system`_ @@ -71,6 +73,8 @@ Configuration * :ref:`enabled ` * `default_locale`_ +* `disallow_search_engine_index`_ +* `error_controller`_ * `esi`_ * :ref:`enabled ` @@ -82,8 +86,62 @@ Configuration * `fragments`_ * :ref:`enabled ` + * `hinclude_default_template`_ * :ref:`path ` +* `http_client`_ + + * :ref:`default_options ` + + * `bindto`_ + * `buffer`_ + * `cafile`_ + * `capath`_ + * `ciphers`_ + * `headers`_ + * `http_version`_ + * `local_cert`_ + * `local_pk`_ + * `max_redirects`_ + * `no_proxy`_ + * `passphrase`_ + * `peer_fingerprint`_ + * `proxy`_ + * `resolve`_ + * `timeout`_ + * `max_duration`_ + * `verify_host`_ + * `verify_peer`_ + + * `max_host_connections`_ + * :ref:`scoped_clients ` + + * `scope`_ + * `auth_basic`_ + * `auth_bearer`_ + * `auth_ntlm`_ + * `base_uri`_ + * `bindto`_ + * `buffer`_ + * `cafile`_ + * `capath`_ + * `ciphers`_ + * `headers`_ + * `http_version`_ + * `local_cert`_ + * `local_pk`_ + * `max_redirects`_ + * `no_proxy`_ + * `passphrase`_ + * `peer_fingerprint`_ + * `proxy`_ + * `query`_ + * `resolve`_ + * `timeout`_ + * `max_duration`_ + * `verify_host`_ + * `verify_peer`_ + * `http_method_override`_ * `ide`_ * :ref:`lock ` @@ -103,12 +161,6 @@ Configuration * `collect`_ * `dsn`_ * :ref:`enabled ` - * `matcher`_ - - * `ip`_ - * :ref:`path ` - * `service`_ - * `only_exceptions`_ * `only_master_requests`_ @@ -116,6 +168,7 @@ Configuration * `magic_call`_ * `throw_exception_on_invalid_index`_ + * `throw_exception_on_invalid_property_path`_ * `property_info`_ @@ -132,11 +185,11 @@ Configuration * `resource`_ * `strict_requirements`_ * :ref:`type ` + * `utf8`_ * `secret`_ * `serializer`_ - * :ref:`cache ` * :ref:`circular_reference_handler ` * :ref:`enable_annotations ` * :ref:`enabled ` @@ -153,6 +206,7 @@ Configuration * `cookie_httponly`_ * `cookie_lifetime`_ * `cookie_path`_ + * `cookie_samesite`_ * `cookie_secure`_ * :ref:`enabled ` * `gc_divisor`_ @@ -162,23 +216,15 @@ Configuration * `metadata_update_threshold`_ * `name`_ * `save_path`_ + * `sid_length`_ + * `sid_bits_per_character`_ * `storage_id`_ * `use_cookies`_ -* `templating`_ - - * :ref:`cache ` - * `engines`_ - * :ref:`form ` - - * :ref:`resources ` - - * `hinclude_default_template`_ - * `loaders`_ - * `test`_ * `translator`_ + * `cache_dir`_ * :ref:`default_path ` * :ref:`enabled ` * `fallbacks`_ @@ -191,14 +237,19 @@ Configuration * `validation`_ * :ref:`cache ` + * `email_validation_mode`_ * :ref:`enable_annotations ` * :ref:`enabled ` * :ref:`mapping ` * :ref:`paths ` + * :ref:`not_compromised_password ` + + * :ref:`enabled ` + * `endpoint`_ + * `static_method`_ - * `strict_email`_ * `translation_domain`_ * `workflows`_ @@ -207,16 +258,17 @@ Configuration * :ref:`name ` * `audit_trail`_ - * `initial_place`_ + * `initial_marking`_ * `marking_store`_ + * `metadata`_ * `places`_ * `supports`_ * `support_strategy`_ * `transitions`_ * :ref:`type ` -``secret`` -~~~~~~~~~~ +secret +~~~~~~ **type**: ``string`` **required** @@ -241,8 +293,8 @@ out all the application users. .. _configuration-framework-http_method_override: -``http_method_override`` -~~~~~~~~~~~~~~~~~~~~~~~~ +http_method_override +~~~~~~~~~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``true`` @@ -254,21 +306,22 @@ named ``kernel.http_method_override``. .. seealso:: - For more information, see :doc:`/form/action_method`. + :ref:`Changing the Action and HTTP Method ` of + Symfony forms. .. caution:: - If you're using the :ref:`AppCache Reverse Proxy ` + If you're using the :ref:`HttpCache Reverse Proxy ` with this option, the kernel will ignore the ``_method`` parameter, which could lead to errors. To fix this, invoke the ``enableHttpMethodParameterOverride()`` method before creating the ``Request`` object:: - // web/app.php + // public/index.php // ... - $kernel = new AppCache($kernel); + $kernel = new CacheKernel($kernel); Request::enableHttpMethodParameterOverride(); // <-- add this line $request = Request::createFromGlobals(); @@ -276,20 +329,21 @@ named ``kernel.http_method_override``. .. _reference-framework-trusted-proxies: -``trusted_proxies`` -~~~~~~~~~~~~~~~~~~~ +trusted_proxies +~~~~~~~~~~~~~~~ The ``trusted_proxies`` option was removed in Symfony 3.3. See :doc:`/deployment/proxies`. -``ide`` -~~~~~~~ +ide +~~~ **type**: ``string`` **default**: ``null`` Symfony turns file paths seen in variable dumps and exception messages into links that open those files right inside your browser. If you prefer to open those files in your favorite IDE or text editor, set this option to any of the -following values: ``phpstorm``, ``sublime``, ``textmate``, ``macvim`` and ``emacs``. +following values: ``phpstorm``, ``sublime``, ``textmate``, ``macvim``, ``emacs``, +``atom`` and ``vscode``. .. note:: @@ -305,13 +359,13 @@ doubling them to prevent Symfony from interpreting them as container parameters) .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: ide: 'myide://open?url=file://%%f&line=%%l' .. code-block:: xml - + loadFromExtension('framework', [ 'ide' => 'myide://open?url=file://%%f&line=%%l', ]); @@ -355,24 +409,23 @@ need to escape the percent signs (``%``) by doubling them. // /path/to/guest/.../file will be opened // as /path/to/host/.../file on the host - // and /foo/.../file as /bar/.../file also - 'myide://%f:%l&/path/to/guest/>/path/to/host/&/foo/>/bar/&...' - - .. versionadded:: 3.2 + // and /var/www/app/ as /projects/my_project/ also + 'myide://%%f:%%l&/path/to/guest/>/path/to/host/&/var/www/app/>/projects/my_project/&...' - Guest to host mappings were introduced in Symfony 3.2. + // example for PhpStorm + 'phpstorm://open?file=%%f&line=%%l&/var/www/app/>/projects/my_project/' .. _reference-framework-test: -``test`` -~~~~~~~~ +test +~~~~ **type**: ``boolean`` If this configuration setting 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``). +``config/packages/test/framework.yaml``). .. seealso:: @@ -380,8 +433,8 @@ setting should be present in your ``test`` environment (usually via .. _config-framework-default_locale: -``default_locale`` -~~~~~~~~~~~~~~~~~~ +default_locale +~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``en`` @@ -395,8 +448,19 @@ method. You can read more information about the default locale in :ref:`translation-default-locale`. -``trusted_hosts`` -~~~~~~~~~~~~~~~~~ +disallow_search_engine_index +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``true`` when the debug mode is enabled, ``false`` otherwise. + +If ``true``, Symfony adds a ``X-Robots-Tag: noindex`` HTTP tag to all responses +(unless your own app adds that header, in which case it's not modified). This +`X-Robots-Tag HTTP header`_ tells search engines to not index your web site. +This option is a protection measure in case you accidentally publish your site +in debug mode. + +trusted_hosts +~~~~~~~~~~~~~ **type**: ``array`` | ``string`` **default**: ``[]`` @@ -423,13 +487,13 @@ the application won't respond and the user will receive a 400 response. .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: trusted_hosts: ['^example\.com$', '^example\.org$'] .. code-block:: xml - + loadFromExtension('framework', [ 'trusted_hosts' => ['^example\.com$', '^example\.org$'], ]); @@ -458,7 +522,7 @@ Hosts can also be configured to respond to any subdomain, via In addition, you can also set the trusted hosts in the front controller using the ``Request::setTrustedHosts()`` method:: - // web/app.php + // public/index.php Request::setTrustedHosts(['^(.+\.)?example\.com$', '^(.+\.)?example\.org$']); The default value for this option is an empty array, meaning that the application @@ -470,15 +534,15 @@ can respond to any given host. .. _reference-framework-form: -``form`` -~~~~~~~~ +form +~~~~ .. _reference-form-enabled: -``enabled`` -........... +enabled +....... -**type**: ``boolean`` **default**: ``false`` +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation Whether to enable the form services or not in the service container. If you don't use forms, setting this to ``false`` may increase your application's @@ -495,30 +559,44 @@ settings is configured. For more details, see :doc:`/forms`. -``csrf_protection`` -~~~~~~~~~~~~~~~~~~~ +.. _reference-framework-csrf-protection: + +csrf_protection +~~~~~~~~~~~~~~~ .. seealso:: - For more information about CSRF protection in forms, see :doc:`/form/csrf_protection`. + For more information about CSRF protection, see :doc:`/security/csrf`. .. _reference-csrf_protection-enabled: -``enabled`` -........... +enabled +....... -**type**: ``boolean`` **default**: ``true`` if form support is enabled, ``false`` -otherwise +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation This option can be used to disable CSRF protection on *all* forms. But you -can also :ref:`disable CSRF protection on individual forms `. +can also :ref:`disable CSRF protection on individual forms `. If you're using forms, but want to avoid starting your session (e.g. using forms in an API-only website), ``csrf_protection`` will need to be set to ``false``. -``esi`` -~~~~~~~ +.. _config-framework-error_controller: + +error_controller +~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``error_controller`` + +This is the controller that is called when an exception is thrown anywhere in +your application. The default controller +(:class:`Symfony\\Component\\HttpKernel\\Controller\\ErrorController`) +renders specific templates under different error conditions (see +:doc:`/controller/error_pages`). + +esi +~~~ .. seealso:: @@ -526,8 +604,8 @@ forms in an API-only website), ``csrf_protection`` will need to be set to .. _reference-esi-enabled: -``enabled`` -........... +enabled +....... **type**: ``boolean`` **default**: ``false`` @@ -539,13 +617,13 @@ You can also set ``esi`` to ``true`` to enable it: .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: esi: true .. code-block:: xml - + loadFromExtension('framework', [ 'esi' => true, ]); -``fragments`` -~~~~~~~~~~~~~ +fragments +~~~~~~~~~ .. seealso:: @@ -576,8 +654,8 @@ You can also set ``esi`` to ``true`` to enable it: .. _reference-fragments-enabled: -``enabled`` -........... +enabled +....... **type**: ``boolean`` **default**: ``false`` @@ -587,121 +665,392 @@ used to render ESI fragments independently of the rest of the page. This setting is automatically set to ``true`` when one of the child settings is configured. +hinclude_default_template +......................... + +**type**: ``string`` **default**: ``null`` + +Sets the content shown during the loading of the fragment or when JavaScript +is disabled. This can be either a template name or the content itself. + +.. seealso:: + + See :doc:`/templating/hinclude` for more information about hinclude. + .. _reference-fragments-path: -``path`` -........ +path +.... **type**: ``string`` **default**: ``'/_fragment'`` The path prefix for fragments. The fragment listener will only be executed when the request starts with this path. -``profiler`` -~~~~~~~~~~~~ +.. _reference-http-client: -.. _reference-profiler-enabled: +http_client +~~~~~~~~~~~ -``enabled`` -........... +When the HttpClient component is installed, an HTTP client is available +as a service named ``http_client`` or using the autowiring alias +:class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface`. -**type**: ``boolean`` **default**: ``false`` +.. _reference-http-client-default-options: -The profiler can be enabled by setting this option to ``true``. When you -are using the Symfony Standard Edition, the profiler is enabled in the ``dev`` -and ``test`` environments. +This service can be configured using ``framework.http_client.default_options``: -.. note:: +.. code-block:: yaml - The profiler works independently from the Web Developer Toolbar, see - the :doc:`WebProfilerBundle configuration ` - on how to disable/enable the toolbar. + # config/packages/framework.yaml + framework: + # ... + http_client: + max_host_connections: 10 + default_options: + headers: { 'X-Powered-By': 'ACME App' } + max_redirects: 7 + +.. _reference-http-client-scoped-clients: + +Multiple pre-configured HTTP client services can be defined, each with its +service name defined as a key under ``scoped_clients``. Scoped clients inherit +the default options defined for the ``http_client`` service. You can override +these options and can define a few others: + +.. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + http_client: + scoped_clients: + my_api.client: + auth_bearer: secret_bearer_token + # ... + +Options defined for scoped clients apply only to URLs that match either their +`base_uri`_ or the `scope`_ option when it is defined. Non-matching URLs always +use default options. + +Each scoped client also defines a corresponding named autowiring alias. +If you use for example +``Symfony\Contracts\HttpClient\HttpClientInterface $myApiClient`` +as the type and name of an argument, autowiring will inject the ``my_api.client`` +service into your autowired classes. -``collect`` +auth_basic +.......... + +**type**: ``string`` + +The username and password used to create the ``Authorization`` HTTP header +used in HTTP Basic authentication. The value of this option must follow the +format ``username:password``. + +auth_bearer ........... -**type**: ``boolean`` **default**: ``true`` +**type**: ``string`` -This option configures the way the profiler behaves when it is enabled. -If set to ``true``, the profiler collects data for all requests (unless -you configure otherwise, like a custom `matcher`_). If you want to only -collect information on-demand, you can set the ``collect`` flag to ``false`` -and activate the data collectors manually:: +The token used to create the ``Authorization`` HTTP header used in HTTP Bearer +authentication (also called token authentication). - $profiler->enable(); +auth_ntlm +......... -``only_exceptions`` -................... +**type**: ``string`` -**type**: ``boolean`` **default**: ``false`` +The username and password used to create the ``Authorization`` HTTP header used +in the `Microsoft NTLM authentication protocol`_. The value of this option must +follow the format ``username:password``. This authentication mechanism requires +using the cURL-based transport. -When this is set to ``true``, the profiler will only be enabled when an -exception is thrown during the handling of the request. +base_uri +........ -``only_master_requests`` -........................ +**type**: ``string`` -**type**: ``boolean`` **default**: ``false`` +URI that is merged into relative URIs, following the rules explained in the +`RFC 3986`_ standard. This is useful when all the requests you make share a +common prefix (e.g. ``https://api.github.com/``) so you can avoid adding it to +every request. -When this is set to ``true``, the profiler will only be enabled on the master -requests (and not on the subrequests). +Here are some common examples of how ``base_uri`` merging works in practice: + +=================== ============== ====================== +``base_uri`` Relative URI Actual Requested URI +=================== ============== ====================== +http://foo.com /bar http://foo.com/bar +http://foo.com/foo /bar http://foo.com/bar +http://foo.com/foo bar http://foo.com/bar +http://foo.com/foo/ bar http://foo.com/foo/bar +http://foo.com http://baz.com http://baz.com +http://foo.com/?bar bar http://foo.com/bar +=================== ============== ====================== + +bindto +...... + +**type**: ``string`` + +A network interface name, IP address, a host name or a UNIX socket to use as the +outgoing network interface. + +buffer +...... + +**type**: ``bool`` | ``Closure`` + +Buffering the response means that you can access its content multiple times +without performing the request again. Buffering is enabled by default when the +content type of the response is ``text/*``, ``application/json`` or ``application/xml``. -``dsn`` +If this option is a boolean value, the response is buffered when the value is +``true``. If this option is a closure, the response is buffered when the +returned value is ``true`` (the closure receives as argument an array with the +response headers). + +cafile +...... + +**type**: ``string`` + +The path of the certificate authority file that contains one or more +certificates used to verify the other servers' certificates. + +capath +...... + +**type**: ``string`` + +The path to a directory that contains one or more certificate authority files. + +ciphers ....... -**type**: ``string`` **default**: ``'file:%kernel.cache_dir%/profiler'`` +**type**: ``string`` -The DSN where to store the profiling information. +A list of the names of the ciphers allowed for the SSL/TLS connections. They +can be separated by colons, commas or spaces (e.g. ``'RC4-SHA:TLS13-AES-128-GCM-SHA256'``). -.. seealso:: +headers +....... - See :doc:`/profiler/storage` for more information about the - profiler storage. +**type**: ``array`` -``matcher`` -........... +An associative array of the HTTP headers added before making the request. This +value must use the format ``['header-name' => header-value, ...]``. -.. caution:: +http_version +............ - This option is deprecated since Symfony 3.4 and will be removed in 4.0. +**type**: ``string`` | ``null`` **default**: ``null`` -Matcher options are configured to dynamically enable the profiler. For -instance, based on the `ip`_ or :ref:`path `. +The HTTP version to use, typically ``'1.1'`` or ``'2.0'``. Leave it to ``null`` +to let Symfony select the best version automatically. -.. seealso:: +local_cert +.......... - See :doc:`/profiler/matchers` for more information about using - matchers to enable/disable the profiler. +**type**: ``string`` -``ip`` -"""""" +The path to a file that contains the `PEM formatted`_ certificate used by the +HTTP client. This is often combined with the ``local_pk`` and ``passphrase`` +options. + +local_pk +........ **type**: ``string`` -If set, the profiler will only be enabled when the current IP address matches. +The path of a file that contains the `PEM formatted`_ private key of the +certificate defined in the ``local_cert`` option. -.. _reference-profiler-matcher-path: +max_host_connections +.................... -``path`` -"""""""" +**type**: ``integer`` **default**: ``6`` + +Defines the maximum amount of simultaneously open connections to a single host +(considering a "host" the same as a "host name + port number" pair). This limit +also applies for proxy connections, where the proxy is considered to be the host +for which this limit is applied. + +max_redirects +............. + +**type**: ``integer`` **default**: ``20`` + +The maximum number of redirects to follow. Use ``0`` to not follow any +redirection. + +no_proxy +........ + +**type**: ``string`` | ``null`` **default**: ``null`` + +A comma separated list of hosts that do not require a proxy to be reached, even +if one is configured. Use the ``'*'`` wildcard to match all hosts and an empty +string to match none (disables the proxy). + +passphrase +.......... **type**: ``string`` -If set, the profiler will only be enabled when the current path matches. +The passphrase used to encrypt the certificate stored in the file defined in the +``local_cert`` option. -``service`` -""""""""""" +peer_fingerprint +................ + +**type**: ``array`` + +When negotiating a TLS or SSL connection, the server sends a certificate +indicating its identity. A public key is extracted from this certificate and if +it does not exactly match any of the public keys provided in this option, the +connection is aborted before sending or receiving any data. + +The value of this option is an associative array of ``algorithm => hash`` +(e.g ``['pin-sha256' => '...']``). + +proxy +..... + +**type**: ``string`` | ``null`` + +The HTTP proxy to use to make the requests. Leave it to ``null`` to detect the +proxy automatically based on your system configuration. + +query +..... + +**type**: ``array`` + +An associative array of the query string values added to the URL before making +the request. This value must use the format ``['parameter-name' => parameter-value, ...]``. + +resolve +....... + +**type**: ``array`` + +A list of hostnames and their IP addresses to pre-populate the DNS cache used by +the HTTP client in order to avoid a DNS lookup for those hosts. This option is +useful to improve security when IPs are checked before the URL is passed to the +client and to make your tests easier. + +The value of this option is an associative array of ``domain => IP address`` +(e.g ``['symfony.com' => '46.137.106.254', ...]``). + +scope +..... **type**: ``string`` -This setting contains the service id of a custom matcher. +For scoped clients only: the regular expression that the URL must match before +applying all other non-default options. By default, the scope is derived from +`base_uri`_. -``request`` -~~~~~~~~~~~ +timeout +....... + +**type**: ``float`` **default**: depends on your PHP config + +Time, in seconds, to wait for a response. If the response stales for longer, a +:class:`Symfony\\Component\\HttpClient\\Exception\\TransportException` is thrown. +Its default value is the same as the value of PHP's `default_socket_timeout`_ +config option. + +max_duration +............ + +**type**: ``float`` **default**: 0 + +The maximum execution time, in seconds, that the request and the response are +allowed to take. A value lower than or equal to 0 means it is unlimited. -``formats`` +verify_host ........... +**type**: ``boolean`` + +If ``true``, the certificate sent by other servers is verified to ensure that +their common name matches the host included in the URL. This is usually +combined with ``verify_peer`` to also verify the certificate authenticity. + +verify_peer +........... + +**type**: ``boolean`` + +If ``true``, the certificate sent by other servers when negotiating a TLS or SSL +connection is verified for authenticity. Authenticating the certificate is not +enough to be sure about the server, so you should combine this with the +``verify_host`` option. + +profiler +~~~~~~~~ + +.. _reference-profiler-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``false`` + +The profiler can be enabled by setting this option to ``true``. When you +install it using Symfony Flex, the profiler is enabled in the ``dev`` +and ``test`` environments. + +.. note:: + + The profiler works independently from the Web Developer Toolbar, see + the :doc:`WebProfilerBundle configuration ` + on how to disable/enable the toolbar. + +collect +....... + +**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 manually:: + + $profiler->enable(); + +only_exceptions +............... + +**type**: ``boolean`` **default**: ``false`` + +When this is set to ``true``, the profiler will only be enabled when an +exception is thrown during the handling of the request. + +only_master_requests +.................... + +**type**: ``boolean`` **default**: ``false`` + +When this is set to ``true``, the profiler will only be enabled on the master +requests (and not on the subrequests). + +dsn +... + +**type**: ``string`` **default**: ``'file:%kernel.cache_dir%/profiler'`` + +The DSN where to store the profiling information. + +request +~~~~~~~ + +formats +....... + **type**: ``array`` **default**: ``[]`` This setting is used to associate additional request formats (e.g. ``html``) @@ -720,7 +1069,7 @@ To configure a ``jsonp`` format: .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: request: formats: @@ -728,7 +1077,7 @@ To configure a ``jsonp`` format: .. code-block:: xml - + loadFromExtension('framework', [ 'request' => [ 'formats' => [ @@ -759,11 +1108,11 @@ To configure a ``jsonp`` format: ], ]); -``router`` -~~~~~~~~~~ +router +~~~~~~ -``resource`` -............ +resource +........ **type**: ``string`` **required** @@ -772,36 +1121,36 @@ routes and imports the router should load. .. _reference-router-type: -``type`` -........ +type +.... **type**: ``string`` The type of the resource to hint the loaders about the format. This isn't needed when you use the default routers with the expected file extensions -(``.xml``, ``.yml`` / ``.yaml``, ``.php``). +(``.xml``, ``.yaml``, ``.php``). -``http_port`` -............. +http_port +......... **type**: ``integer`` **default**: ``80`` The port for normal http requests (this is used when matching the scheme). -``https_port`` -.............. +https_port +.......... **type**: ``integer`` **default**: ``443`` The port for https requests (this is used when matching the scheme). -``strict_requirements`` -....................... +strict_requirements +................... **type**: ``mixed`` **default**: ``true`` Determines the routing generator behavior. When generating a route that -has specific :doc:`requirements `, the generator +has specific :ref:`parameter requirements `, the generator can behave differently in case the used parameters do not meet these requirements. The value can be one of: @@ -818,11 +1167,33 @@ The value can be one of: ``true`` is recommended in the development environment, while ``false`` or ``null`` might be preferred in production. -``session`` -~~~~~~~~~~~ +utf8 +.... -``storage_id`` -.............. +**type**: ``boolean`` **default**: ``false`` + +.. deprecated:: 5.1 + + Not setting this option is deprecated since Symfony 5.1. Moreover, the + default value of this option will change to ``true`` in Symfony 6.0. + +When this option is set to ``true``, the regular expressions used in the +:ref:`requirements of route parameters ` will be run +using the `utf-8 modifier`_. This will for example match any UTF-8 character +when using ``.``, instead of matching only a single byte. + +If the charset of your application is UTF-8 (as defined in the +:ref:`getCharset() method ` of your kernel) it's +recommended to set it to ``true``. This will make non-UTF8 URLs to generate 404 +errors. + +.. _config-framework-session: + +session +~~~~~~~ + +storage_id +.......... **type**: ``string`` **default**: ``'session.storage.native'`` @@ -830,26 +1201,23 @@ The service id used for session storage. The ``session.storage`` service alias will be set to this service id. This class has to implement :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface`. -``handler_id`` -.............. - -**type**: ``string`` **default**: ``'session.handler.native_file'`` +handler_id +.......... -The service id used for session storage. The ``session.handler`` service -alias will be set to this service id. +**type**: ``string`` **default**: ``null`` -You can also set it to ``null``, to default to the handler of your PHP -installation. +The service id used for session storage. The default ``null`` value means to use +the native PHP session mechanism. Set it to ``'session.handler.native_file'`` to +let Symfony manage the sessions itself using files to store the session +metadata. -.. seealso:: - - You can see an example of the usage of this in - :doc:`/doctrine/pdo_session_storage`. +If you prefer to make Symfony store sessions in a database read +:doc:`/doctrine/pdo_session_storage`. .. _name: -``name`` -........ +name +.... **type**: ``string`` **default**: ``null`` @@ -857,8 +1225,8 @@ 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. -``cookie_lifetime`` -................... +cookie_lifetime +............... **type**: ``integer`` **default**: ``null`` @@ -867,16 +1235,16 @@ This determines the lifetime of the session - in seconds. The default value will be used. Setting this value to ``0`` means the cookie is valid for the length of the browser session. -``cookie_path`` -............... +cookie_path +........... **type**: ``string`` **default**: ``/`` This determines the path to set in the session cookie. By default, it will use ``/``. -``cache_limiter`` -................. +cache_limiter +............. **type**: ``string`` or ``int`` **default**: ``''`` @@ -885,20 +1253,20 @@ and it will rely on the cache control method configured in the `session.cache-limiter`_ PHP.ini option. Unlike the other session options, ``cache_limiter`` is set as a regular -:doc:`container parameter `: +:ref:`container parameter `: .. configuration-block:: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml parameters: session.storage.options: cache_limiter: 0 .. code-block:: xml - + setParameter('session.storage.options', [ 'cache_limiter' => 0, ]); -``cookie_domain`` -................. +cookie_domain +............. **type**: ``string`` **default**: ``''`` @@ -928,15 +1296,48 @@ 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. -``cookie_secure`` -................. +cookie_samesite +............... -**type**: ``boolean`` **default**: ``false`` +**type**: ``string`` or ``null`` **default**: ``'lax'`` -This determines whether cookies should only be sent over secure connections. +It controls the way cookies are sent when the HTTP request was not originated +from the same domain the cookies are associated to. Setting this option is +recommended to mitigate `CSRF security attacks`_. -``cookie_httponly`` -................... +By default, browsers send all cookies related to the domain of the HTTP request. +This may be a problem for example when you visit a forum and some malicious +comment includes a link like ``https://some-bank.com/?send_money_to=attacker&amount=1000``. +If you were previously logged into your bank website, the browser will send all +those cookies when making that HTTP request. + +The possible values for this option are: + +* ``null``, use it to disable this protection. Same behavior as in older Symfony + versions. +* ``'strict'`` (or the ``Cookie::SAMESITE_STRICT`` constant), use it to never + send any cookie when the HTTP request is not originated from the same domain. +* ``'lax'`` (or the ``Cookie::SAMESITE_LAX`` constant), use it to allow sending + cookies when the request originated from a different domain, but only when the + user consciously made the request (by clicking a link or submitting a form + with the ``GET`` method). + +.. note:: + + This option is available starting from PHP 7.3, but Symfony has a polyfill + so you can use it with any older PHP version as well. + +cookie_secure +............. + +**type**: ``boolean`` or ``null`` **default**: ``null`` + +This determines whether cookies should only be sent over secure connections. In +addition to ``true`` and ``false``, there's a special ``null`` value that +means ``true`` for HTTPS requests and ``false`` for HTTP requests. + +cookie_httponly +............... **type**: ``boolean`` **default**: ``true`` @@ -945,15 +1346,15 @@ 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. -``gc_divisor`` -.............. +gc_divisor +.......... **type**: ``integer`` **default**: ``100`` See `gc_probability`_. -``gc_probability`` -.................. +gc_probability +.............. **type**: ``integer`` **default**: ``1`` @@ -962,8 +1363,8 @@ started on every session initialization. The probability is calculated by using ``gc_probability`` / ``gc_divisor``, e.g. 1/100 means there is a 1% chance that the GC process will start on each request. -``gc_maxlifetime`` -.................. +gc_maxlifetime +.............. **type**: ``integer`` **default**: ``1440`` @@ -971,14 +1372,36 @@ This determines the number of seconds after which data will be seen as "garbage" and potentially cleaned up. Garbage collection may occur during session start and depends on `gc_divisor`_ and `gc_probability`_. -``save_path`` -............. +sid_length +.......... + +**type**: ``integer`` **default**: ``32`` + +This determines the length of session ID string, which can be an integer between +``22`` and ``256`` (both inclusive), being ``32`` the recommended value. Longer +session IDs are harder to guess. + +This option is related to the `session.sid_length PHP option`_. + +sid_bits_per_character +...................... + +**type**: ``integer`` **default**: ``4`` + +This determines the number of bits in encoded session ID character. The possible +values are ``4`` (0-9, a-f), ``5`` (0-9, a-v), and ``6`` (0-9, a-z, A-Z, "-", ","). +The more bits results in stronger session ID. ``5`` is recommended value for +most environments. + +This option is related to the `session.sid_bits_per_character PHP option`_. + +save_path +......... **type**: ``string`` **default**: ``%kernel.cache_dir%/sessions`` This determines the argument to be passed to the save handler. If you choose the default file handler, this is the path where the session files are created. -For more information, see :doc:`/session/sessions_directory`. You can also set this value to the ``save_path`` of your ``php.ini`` by setting the value to ``null``: @@ -987,14 +1410,14 @@ setting the value to ``null``: .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: session: save_path: ~ .. code-block:: xml - + loadFromExtension('framework', [ 'session' => [ 'save_path' => null, @@ -1019,8 +1442,8 @@ setting the value to ``null``: .. _reference-session-metadata-update-threshold: -``metadata_update_threshold`` -............................. +metadata_update_threshold +......................... **type**: ``integer`` **default**: ``0`` @@ -1033,8 +1456,8 @@ changed. Previously, you needed to set this option to avoid that behavior. .. _reference-session-enabled: -``enabled`` -........... +enabled +....... **type**: ``boolean`` **default**: ``true`` @@ -1044,14 +1467,14 @@ Whether to enable the session support in the framework. .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: session: enabled: true .. code-block:: xml - + loadFromExtension('framework', [ 'session' => [ 'enabled' => true, ], ]); -``use_cookies`` -............... +use_cookies +........... **type**: ``boolean`` **default**: ``null`` @@ -1083,13 +1506,13 @@ This specifies if the session ID is stored on the client side using cookies or not. By default, it will use the value defined in the ``php.ini`` with the ``session.use_cookies`` directive. -``assets`` -~~~~~~~~~~ +assets +~~~~~~ .. _reference-assets-base-path: -``base_path`` -............. +base_path +......... **type**: ``string`` @@ -1099,7 +1522,7 @@ This option allows you to define a base path to be used for assets: .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: # ... assets: @@ -1107,7 +1530,7 @@ This option allows you to define a base path to be used for assets: .. code-block:: xml - + loadFromExtension('framework', [ // ... 'assets' => [ @@ -1134,8 +1557,8 @@ This option allows you to define a base path to be used for assets: .. _reference-templating-base-urls: .. _reference-assets-base-urls: -``base_urls`` -............. +base_urls +......... **type**: ``array`` @@ -1147,7 +1570,7 @@ collection each time it generates an asset's path: .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: # ... assets: @@ -1156,7 +1579,7 @@ collection each time it generates an asset's path: .. code-block:: xml - + loadFromExtension('framework', [ // ... 'assets' => [ @@ -1182,8 +1605,8 @@ collection each time it generates an asset's path: .. _reference-framework-assets-packages: -``packages`` -............ +packages +........ You can group assets into packages, to specify different base URLs for them: @@ -1191,7 +1614,7 @@ You can group assets into packages, to specify different base URLs for them: .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: # ... assets: @@ -1201,7 +1624,7 @@ You can group assets into packages, to specify different base URLs for them: .. code-block:: xml - + loadFromExtension('framework', [ // ... 'assets' => [ @@ -1251,8 +1674,8 @@ Each package can configure the following options: .. _reference-framework-assets-version: .. _ref-framework-assets-version: -``version`` -........... +version +....... **type**: ``string`` @@ -1274,7 +1697,7 @@ Now, activate the ``version`` option: .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: # ... assets: @@ -1282,7 +1705,7 @@ Now, activate the ``version`` option: .. code-block:: xml - + loadFromExtension('framework', [ // ... 'assets' => [ @@ -1326,8 +1749,8 @@ option. .. _reference-templating-version-format: .. _reference-assets-version-format: -``version_format`` -.................. +version_format +.............. **type**: ``string`` **default**: ``%%s?%%s`` @@ -1366,8 +1789,8 @@ is set to ``5``, the asset's path would be ``/images/logo.png?version=5``. .. _reference-assets-version-strategy: .. _reference-templating-version-strategy: -``version_strategy`` -.................... +version_strategy +................ **type**: ``string`` **default**: ``null`` @@ -1379,7 +1802,7 @@ individually for each asset package: .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: assets: # this strategy is applied to every asset (including packages) @@ -1397,7 +1820,7 @@ individually for each asset package: .. code-block:: xml - + loadFromExtension('framework', [ 'assets' => [ 'version_strategy' => 'app.asset.my_versioning_strategy', @@ -1451,16 +1874,12 @@ individually for each asset package: This parameter cannot be set at the same time as ``version`` or ``json_manifest_path``. .. _reference-assets-json-manifest-path: -.. _reference-templating-json-manifest-path: - -``json_manifest_path`` -...................... - -**type**: ``string`` **default**: ``null`` +.. _reference-templating-json-manifest-path: -.. versionadded:: 3.3 +json_manifest_path +.................. - The ``json_manifest_path`` option was introduced in Symfony 3.3. +**type**: ``string`` **default**: ``null`` The file path to a ``manifest.json`` file containing an associative array of asset names and their respective compiled names. A common cache-busting technique using @@ -1481,22 +1900,22 @@ package: .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: assets: # this manifest is applied to every asset (including packages) - json_manifest_path: "%kernel.project_dir%/web/assets/manifest.json" + json_manifest_path: "%kernel.project_dir%/public/build/manifest.json" packages: foo_package: # this package uses its own manifest (the default file is ignored) - json_manifest_path: "%kernel.project_dir%/web/assets/a_different_manifest.json" + json_manifest_path: "%kernel.project_dir%/public/build/a_different_manifest.json" bar_package: # this package uses the global manifest (the default file is used) base_path: '/images' .. code-block:: xml - + - + + json-manifest-path="%kernel.project_dir%/public/build/a_different_manifest.json"/> loadFromExtension('framework', [ 'assets' => [ // this manifest is applied to every asset (including packages) - 'json_manifest_path' => '%kernel.project_dir%/web/assets/manifest.json', + 'json_manifest_path' => '%kernel.project_dir%/public/build/manifest.json', 'packages' => [ 'foo_package' => [ // this package uses its own manifest (the default file is ignored) - 'json_manifest_path' => '%kernel.project_dir%/web/assets/a_different_manifest.json', + 'json_manifest_path' => '%kernel.project_dir%/public/build/a_different_manifest.json', ], 'bar_package' => [ // this package uses the global manifest (the default file is used) @@ -1550,152 +1969,32 @@ package: If you request an asset that is *not found* in the ``manifest.json`` file, the original - *unmodified* - asset path will be returned. -``templating`` -~~~~~~~~~~~~~~ - -``hinclude_default_template`` -............................. - -**type**: ``string`` **default**: ``null`` - -Sets the content shown during the loading of the fragment or when JavaScript -is disabled. This can be either a template name or the content itself. - -.. seealso:: - - See :doc:`/templating/hinclude` for more information about HInclude. - -.. _reference-templating-form: - -``form`` -........ - -.. _reference-templating-form-resources: - -``resources`` -""""""""""""" - -**type**: ``string[]`` **default**: ``['FrameworkBundle:Form']`` - -A list of all resources for form theming in PHP. This setting is not required -if you're using the Twig format for your templates, in that case refer to -:ref:`the form article `. - -Assume you have custom global form themes in -``src/WebsiteBundle/Resources/views/Form``, you can configure this like: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - templating: - form: - resources: - - 'WebsiteBundle:Form' - - .. code-block:: xml - - - - - - - - - - - - WebsiteBundle:Form - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', [ - 'templating' => [ - 'form' => [ - 'resources' => [ - 'WebsiteBundle:Form', - ], - ], - ], - ]); - -.. note:: - - The default form templates from ``FrameworkBundle:Form`` will always - be included in the form resources. - -.. seealso:: - - See :ref:`forms-theming-global` for more information. - -.. _reference-templating-cache: +translator +~~~~~~~~~~ -``cache`` +cache_dir ......... -**type**: ``string`` - -The path to the cache directory for templates. When this is not set, caching -is disabled. - -.. note:: - - When using Twig templating, the caching is already handled by the - TwigBundle and doesn't need to be enabled for the FrameworkBundle. - -``engines`` -........... - -**type**: ``string[]`` / ``string`` **required** +**type**: ``string`` | ``null`` **default**: ``%kernel.cache_dir%/translations/`` -The Templating Engine to use. This can either be a string (when only one -engine is configured) or an array of engines. - -At least one engine is required. - -``loaders`` -........... - -**type**: ``string[]`` - -An array (or a string when configuring just one loader) of service ids for -templating loaders. Templating loaders are used to find and load templates -from a resource (e.g. a filesystem or database). Templating loaders must -implement :class:`Symfony\\Component\\Templating\\Loader\\LoaderInterface`. - -``translator`` -~~~~~~~~~~~~~~ +Defines the directory where the translation cache is stored. Use ``null`` to +disable this cache. .. _reference-translator-enabled: -``enabled`` -........... +enabled +....... -**type**: ``boolean`` **default**: ``false`` +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation Whether or not to enable the ``translator`` service in the service container. .. _fallback: -``fallbacks`` -............. +fallbacks +......... -**type**: ``string|array`` **default**: ``['en']`` +**type**: ``string|array`` **default**: value of `default_locale`_ This option is used when the translation key for the current locale wasn't found. @@ -1706,8 +2005,8 @@ found. .. _reference-framework-translator-logging: -``logging`` -........... +logging +....... **default**: ``true`` when the debug mode is enabled, ``false`` otherwise. @@ -1718,8 +2017,8 @@ locale and the ``warning`` level if there is no translation to use at all. .. _reference-framework-translator-formatter: -``formatter`` -............. +formatter +......... **type**: ``string`` **default**: ``translator.formatter.default`` @@ -1728,8 +2027,8 @@ must implement the :class:`Symfony\\Component\\Translation\\Formatter\\MessageFo .. _reference-translator-paths: -``paths`` -......... +paths +..... **type**: ``array`` **default**: ``[]`` @@ -1738,23 +2037,19 @@ for translation files. .. _reference-translator-default_path: -``default_path`` -................ - -.. versionadded:: 3.4 - - The ``default_path`` option was introduced in Symfony 3.4. +default_path +............ **type**: ``string`` **default**: ``%kernel.project_dir%/translations`` This option allows to define the path where the application translations files are stored. -``property_access`` -~~~~~~~~~~~~~~~~~~~ +property_access +~~~~~~~~~~~~~~~ -``magic_call`` -.............. +magic_call +.......... **type**: ``boolean`` **default**: ``false`` @@ -1762,34 +2057,41 @@ When enabled, the ``property_accessor`` service uses PHP's :ref:`magic __call() method ` when its ``getValue()`` method is called. -``throw_exception_on_invalid_index`` -.................................... +throw_exception_on_invalid_index +................................ **type**: ``boolean`` **default**: ``false`` When enabled, the ``property_accessor`` service throws an exception when you try to access an invalid index of an array. -``property_info`` -~~~~~~~~~~~~~~~~~ +throw_exception_on_invalid_property_path +........................................ + +**type**: ``boolean`` **default**: ``true`` + +When enabled, the ``property_accessor`` service throws an exception when you +try to access an invalid property path of an object. + +property_info +~~~~~~~~~~~~~ .. _reference-property-info-enabled: -``enabled`` -........... +enabled +....... -**type**: ``boolean`` **default**: ``false`` +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation -``validation`` -~~~~~~~~~~~~~~ +validation +~~~~~~~~~~ .. _reference-validation-enabled: -``enabled`` -........... +enabled +....... -**type**: ``boolean`` **default**: ``true`` if :ref:`form support is enabled `, -``false`` otherwise +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation Whether or not to enable validation support. @@ -1798,8 +2100,8 @@ settings is configured. .. _reference-validation-cache: -``cache`` -......... +cache +..... **type**: ``string`` @@ -1811,23 +2113,55 @@ cache provide from the Doctrine project. .. _reference-validation-enable_annotations: -``enable_annotations`` -...................... +enable_annotations +.................. **type**: ``boolean`` **default**: ``false`` If this option is enabled, validation constraints can be defined using annotations. -``translation_domain`` -...................... +translation_domain +.................. **type**: ``string`` **default**: ``validators`` The translation domain that is used when translating validation constraint error messages. -``static_method`` -................. +.. _reference-validation-not-compromised-password: + +not_compromised_password +........................ + +The :doc:`NotCompromisedPassword ` +constraint makes HTTP requests to a public API to check if the given password +has been compromised in a data breach. + +.. _reference-validation-not-compromised-password-enabled: + +enabled +""""""" + +**type**: ``boolean`` **default**: ``true`` + +If you set this option to ``false``, no HTTP requests will be made and the given +password will be considered valid. This is useful when you don't want or can't +make HTTP requests, such as in ``dev`` and ``test`` environments or in +continuous integration servers. + +endpoint +"""""""" + +**type**: ``string`` **default**: ``null`` + +By default, the :doc:`NotCompromisedPassword ` +constraint uses the public API provided by `haveibeenpwned.com`_. This option +allows to define a different, but compatible, API endpoint to make the password +checks. It's useful for example when the Symfony application is run in an +intranet without public access to Internet. + +static_method +............. **type**: ``string | array`` **default**: ``['loadValidatorMetadata']`` @@ -1836,37 +2170,89 @@ metadata of the class. You can define an array of strings with the names of several methods. In that case, all of them will be called in that order to load the metadata. -``strict_email`` -................ +email_validation_mode +..................... -**type**: ``Boolean`` **default**: ``false`` +**type**: ``string`` **default**: ``loose`` -If this option is enabled, the `egulias/email-validator`_ library will be -used by the :doc:`/reference/constraints/Email` constraint validator. Otherwise, -the validator uses a simple regular expression to validate email addresses. +It controls the way email addresses are validated by the +:doc:`/reference/constraints/Email` validator. The possible values are: + +* ``loose``, it uses a simple regular expression to validate the address (it + checks that at least one ``@`` character is present, etc.). This validation is + too simple and it's recommended to use the ``html5`` validation instead; +* ``html5``, it validates email addresses using the same regular expression + defined in the HTML5 standard, making the backend validation consistent with + the one provided by browsers; +* ``strict``, it uses the `egulias/email-validator`_ library (which you must + install separately) to validate the addresses according to the `RFC 5322`_. .. _reference-validation-mapping: -``mapping`` -........... +mapping +....... .. _reference-validation-mapping-paths: -``paths`` -""""""""" +paths +""""" **type**: ``array`` **default**: ``[]`` This option allows to define an array of paths with files or directories where -the component will look for additional validation files. +the component will look for additional validation files: -``annotations`` -~~~~~~~~~~~~~~~ +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + validation: + mapping: + paths: + - "%kernel.project_dir%/validation/" + + .. code-block:: xml + + + + + + + + + %kernel.project_dir%/validation + + + + + + .. code-block:: php + + // config/packages/framework.php + $container->loadFromExtension('framework', [ + 'validation' => [ + 'mapping' => [ + 'paths' => [ + '%kernel.project_dir%/validation', + ], + ], + ], + ]); + +annotations +~~~~~~~~~~~ .. _reference-annotations-cache: -``cache`` -......... +cache +..... **type**: ``string`` **default**: ``'file'`` @@ -1879,16 +2265,16 @@ none a service id A service id referencing a `Doctrine Cache`_ implementation -``file_cache_dir`` -.................. +file_cache_dir +.............. **type**: ``string`` **default**: ``'%kernel.cache_dir%/annotations'`` The directory to store cache files for annotations, in case ``annotations.cache`` is set to ``'file'``. -``debug`` -......... +debug +..... **type**: ``boolean`` **default**: ``%kernel.debug%`` @@ -1900,36 +2286,22 @@ default value. .. _configuration-framework-serializer: -``serializer`` -~~~~~~~~~~~~~~ +serializer +~~~~~~~~~~ .. _reference-serializer-enabled: -``enabled`` -........... +enabled +....... -**type**: ``boolean`` **default**: ``false`` +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation Whether to enable the ``serializer`` service or not in the service container. -.. _reference-serializer-cache: - -``cache`` -......... - -**type**: ``string`` - -The service that is used to persist class metadata in a cache. The service -has to implement the ``Doctrine\Common\Cache\Cache`` interface. - -.. seealso:: - - For more information, see :ref:`serializer-enabling-metadata-cache`. - .. _reference-serializer-enable_annotations: -``enable_annotations`` -...................... +enable_annotations +.................. **type**: ``boolean`` **default**: ``false`` @@ -1941,8 +2313,8 @@ If this option is enabled, serialization groups can be defined using annotations .. _reference-serializer-name_converter: -``name_converter`` -.................. +name_converter +.............. **type**: ``string`` @@ -1958,8 +2330,8 @@ value. .. _reference-serializer-circular_reference_handler: -``circular_reference_handler`` -.............................. +circular_reference_handler +.......................... **type** ``string`` @@ -1974,39 +2346,33 @@ method. .. _reference-serializer-mapping: -``mapping`` -........... +mapping +....... .. _reference-serializer-mapping-paths: -``paths`` -""""""""" +paths +""""" **type**: ``array`` **default**: ``[]`` This option allows to define an array of paths with files or directories where the component will look for additional serialization files. -``php_errors`` -~~~~~~~~~~~~~~ - -``log`` -....... - -.. versionadded:: 3.2 +php_errors +~~~~~~~~~~ - The ``log`` option was introduced in Symfony 3.2. +log +... -**type**: ``boolean`` **default**: ``%kernel.debug%`` +**type**: ``boolean|int`` **default**: ``%kernel.debug%`` Use the application logger instead of the PHP logger for logging PHP errors. +When an integer value is used, it also sets the log level. Those integer +values must be the same used in the `error_reporting PHP option`_. -``throw`` -......... - -.. versionadded:: 3.2 - - The ``throw`` option was introduced in Symfony 3.2. +throw +..... **type**: ``boolean`` **default**: ``%kernel.debug%`` @@ -2015,20 +2381,24 @@ Throw PHP errors as ``\ErrorException`` instances. The parameter .. _reference-cache: -``cache`` -~~~~~~~~~ +cache +~~~~~ .. _reference-cache-app: -``app`` -....... +app +... **type**: ``string`` **default**: ``cache.adapter.filesystem`` The cache adapter used by the ``cache.app`` service. The FrameworkBundle ships with multiple adapters: ``cache.adapter.apcu``, ``cache.adapter.doctrine``, ``cache.adapter.system``, ``cache.adapter.filesystem``, ``cache.adapter.psr6``, -``cache.adapter.redis`` and ``cache.adapter.memcached``. +``cache.adapter.redis``, ``cache.adapter.memcached`` and ``cache.adapter.pdo``. + +There's also a special adapter called ``cache.adapter.array`` which stores +contents in memory using a PHP array and it's used to disable caching (mostly on +the ``dev`` environment). .. tip:: @@ -2039,60 +2409,65 @@ ships with multiple adapters: ``cache.adapter.apcu``, ``cache.adapter.doctrine`` .. _reference-cache-system: -``system`` -.......... +system +...... **type**: ``string`` **default**: ``cache.adapter.system`` The cache adapter used by the ``cache.system`` service. It supports the same adapters available for the ``cache.app`` service. -``directory`` -............. +directory +......... **type**: ``string`` **default**: ``%kernel.cache_dir%/pools`` The path to the cache directory used by services inheriting from the ``cache.adapter.filesystem`` adapter (including ``cache.app``). -``default_doctrine_provider`` -............................. +default_doctrine_provider +......................... **type**: ``string`` The service name to use as your default Doctrine provider. The provider is available as the ``cache.default_doctrine_provider`` service. -``default_psr6_provider`` -......................... +default_psr6_provider +..................... **type**: ``string`` The service name to use as your default PSR-6 provider. It is available as the ``cache.default_psr6_provider`` service. -``default_redis_provider`` -.......................... +default_redis_provider +...................... **type**: ``string`` **default**: ``redis://localhost`` The DSN to use by the Redis provider. The provider is available as the ``cache.default_redis_provider`` service. -``default_memcached_provider`` -.............................. - -.. versionadded:: 3.3 - - The ``default_memcached_provider`` option was introduced in Symfony 3.3. +default_memcached_provider +.......................... **type**: ``string`` **default**: ``memcached://localhost`` The DSN to use by the Memcached provider. The provider is available as the ``cache.default_memcached_provider`` service. -``pools`` -......... +default_pdo_provider +.................... + +**type**: ``string`` **default**: ``doctrine.dbal.default_connection`` + +The service id of the database connection, which should be either a PDO or a +Doctrine DBAL instance. The provider is available as the ``cache.default_pdo_provider`` +service. + +pools +..... **type**: ``array`` @@ -2108,7 +2483,7 @@ To configure a Redis cache pool with a default lifetime of 1 hour, do the follow .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: cache: pools: @@ -2118,7 +2493,7 @@ To configure a Redis cache pool with a default lifetime of 1 hour, do the follow .. code-block:: xml - + loadFromExtension('framework', [ 'cache' => [ 'pools' => [ @@ -2155,8 +2530,8 @@ To configure a Redis cache pool with a default lifetime of 1 hour, do the follow .. _reference-cache-pools-name: -``name`` -"""""""" +name +"""" **type**: ``prototype`` @@ -2166,8 +2541,8 @@ Name of the pool you want to create. Your pool name must differ from ``cache.app`` or ``cache.system``. -``adapter`` -""""""""""" +adapter +""""""" **type**: ``string`` **default**: ``cache.app`` @@ -2180,22 +2555,30 @@ settings from the base pool as defaults. Your service MUST implement the ``Psr\Cache\CacheItemPoolInterface`` interface. -``public`` -"""""""""" +public +"""""" **type**: ``boolean`` **default**: ``false`` Whether your service should be public or not. -``default_lifetime`` -"""""""""""""""""""" +tags +"""" + +**type**: ``boolean`` | ``string`` **default**: ``null`` + +Whether your service should be able to handle tags or not. +Can also be the service id of another cache pool where tags will be stored. + +default_lifetime +"""""""""""""""" **type**: ``integer`` Default lifetime of your cache items in seconds. -``provider`` -"""""""""""" +provider +"""""""" **type**: ``string`` @@ -2204,8 +2587,8 @@ use what is configured as ``default_X_provider`` under ``cache``. See the description of the default provider setting above for the type of adapter you use for information on how to specify the provider. -``clearer`` -""""""""""" +clearer +""""""" **type**: ``string`` @@ -2215,12 +2598,10 @@ The cache clearer used to clear your PSR-6 cache. For more information, see :class:`Symfony\\Component\\HttpKernel\\CacheClearer\\Psr6CacheClearer`. -``prefix_seed`` -............... - -.. versionadded:: 3.2 +.. _reference-cache-prefix-seed: - The ``prefix_seed`` option was introduced in Symfony 3.2. +prefix_seed +........... **type**: ``string`` **default**: ``null`` @@ -2236,8 +2617,8 @@ example, when warming caches offline). .. _reference-lock: -``lock`` -~~~~~~~~ +lock +~~~~ **type**: ``string`` | ``array`` @@ -2246,8 +2627,8 @@ available, or to ``flock`` otherwise. Store's DSN are also allowed. .. _reference-lock-enabled: -``enabled`` -........... +enabled +....... **type**: ``boolean`` **default**: ``true`` @@ -2256,8 +2637,8 @@ automatically set to ``true`` when one of the child settings is configured. .. _reference-lock-resources: -``resources`` -............. +resources +......... **type**: ``array`` @@ -2267,11 +2648,12 @@ A list of lock stores to be created by the framework extension. .. code-block:: yaml - # app/config/config.yml + # config/packages/lock.yaml framework: # these are all the supported lock stores lock: ~ lock: 'flock' + lock: 'flock:///path/to/file' lock: 'semaphore' lock: 'memcached://m1.docker' lock: ['memcached://m1.docker', 'memcached://m2.docker'] @@ -2286,7 +2668,7 @@ A list of lock stores to be created by the framework extension. .. code-block:: xml - + flock + flock:///path/to/file + semaphore memcached://m1.docker @@ -2324,11 +2708,12 @@ A list of lock stores to be created by the framework extension. .. code-block:: php - // app/config/config.php + // config/packages/lock.php $container->loadFromExtension('framework', [ // these are all the supported lock stores 'lock' => null, 'lock' => 'flock', + 'lock' => 'flock:///path/to/file', 'lock' => 'semaphore', 'lock' => 'memcached://m1.docker', 'lock' => ['memcached://m1.docker', 'memcached://m2.docker'], @@ -2345,8 +2730,8 @@ A list of lock stores to be created by the framework extension. .. _reference-lock-resources-name: -``name`` -"""""""" +name +"""" **type**: ``prototype`` @@ -2364,8 +2749,8 @@ Name of the lock you want to create. decorates: lock.invoice.store arguments: ['@lock.invoice.retry_till_save.store.inner', 100, 50] -``workflows`` -~~~~~~~~~~~~~ +workflows +~~~~~~~~~ **type**: ``array`` @@ -2375,7 +2760,7 @@ A list of workflows to be created by the framework extension: .. code-block:: yaml - # app/config/config.yml + # config/packages/workflow.yaml framework: workflows: my_workflow: @@ -2383,7 +2768,7 @@ A list of workflows to be created by the framework extension: .. code-block:: xml - + loadFromExtension('framework', [ 'workflows' => [ 'my_workflow' => // ... @@ -2416,8 +2801,8 @@ A list of workflows to be created by the framework extension: .. _reference-workflows-enabled: -``enabled`` -........... +enabled +....... **type**: ``boolean`` **default**: ``false`` @@ -2426,31 +2811,31 @@ automatically set to ``true`` when one of the child settings is configured. .. _reference-workflows-name: -``name`` -........ +name +.... **type**: ``prototype`` Name of the workflow you want to create. -``audit_trail`` -""""""""""""""" +audit_trail +""""""""""" **type**: ``bool`` If set to ``true``, the :class:`Symfony\\Component\\Workflow\\EventListener\\AuditTrailListener` will be enabled. -``initial_place`` -""""""""""""""""" +initial_marking +""""""""""""""" -**type**: ``string`` **default**: ``null`` +**type**: ``string`` | ``array`` -One of the ``places`` or ``null``. If not null and the supported object is not +One of the ``places`` or ``empty``. If not null and the supported object is not already initialized via the workflow, this place will be set. -``marking_store`` -""""""""""""""""" +marking_store +""""""""""""" **type**: ``array`` @@ -2458,31 +2843,39 @@ Each marking store can define any of these options: * ``arguments`` (**type**: ``array``) * ``service`` (**type**: ``string``) -* ``type`` (**type**: ``string`` **possible values**: ``'multiple_state'`` or - ``'single_state'``) +* ``type`` (**type**: ``string`` **allow value**: ``'method'``) + +metadata +"""""""" -``places`` -"""""""""" +**type**: ``array`` + +Metadata available for the workflow configuration. +Note that ``places`` and ``transitions`` can also have their own +``metadata`` entry. + +places +"""""" **type**: ``array`` All available places (**type**: ``string``) for the workflow configuration. -``supports`` -"""""""""""" +supports +"""""""" **type**: ``string`` | ``array`` The FQCN (fully-qualified class name) of the object supported by the workflow configuration or an array of FQCN if multiple objects are supported. -``support_strategy`` -"""""""""""""""""""" +support_strategy +"""""""""""""""" **type**: ``string`` -``transitions`` -""""""""""""""" +transitions +""""""""""" **type**: ``array`` @@ -2498,8 +2891,8 @@ Each marking store can define any of these options: .. _reference-workflows-type: -``type`` -"""""""" +type +"""" **type**: ``string`` **possible values**: ``'workflow'`` or ``'state_machine'`` @@ -2511,9 +2904,21 @@ to know their differences. .. _`Security Advisory Blog post`: https://symfony.com/blog/security-releases-symfony-2-0-24-2-1-12-2-2-5-and-2-3-3-released#cve-2013-4752-request-gethost-poisoning .. _`Doctrine Cache`: http://docs.doctrine-project.org/projects/doctrine-common/en/latest/reference/caching.html .. _`egulias/email-validator`: https://github.com/egulias/EmailValidator +.. _`RFC 5322`: https://tools.ietf.org/html/rfc5322 .. _`PhpStormProtocol`: https://github.com/aik099/PhpStormProtocol .. _`phpstorm-url-handler`: https://github.com/sanduhrs/phpstorm-url-handler .. _`blue/green deployment`: http://martinfowler.com/bliki/BlueGreenDeployment.html .. _`gulp-rev`: https://www.npmjs.com/package/gulp-rev .. _`webpack-manifest-plugin`: https://www.npmjs.com/package/webpack-manifest-plugin +.. _`error_reporting PHP option`: https://secure.php.net/manual/en/errorfunc.configuration.php#ini.error-reporting +.. _`CSRF security attacks`: https://en.wikipedia.org/wiki/Cross-site_request_forgery +.. _`session.sid_length PHP option`: https://php.net/manual/session.configuration.php#ini.session.sid-length +.. _`session.sid_bits_per_character PHP option`: https://php.net/manual/session.configuration.php#ini.session.sid-bits-per-character +.. _`X-Robots-Tag HTTP header`: https://developers.google.com/search/reference/robots_meta_tag +.. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt +.. _`default_socket_timeout`: https://php.net/manual/en/filesystem.configuration.php#ini.default-socket-timeout +.. _`PEM formatted`: https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail +.. _`haveibeenpwned.com`: https://haveibeenpwned.com/ .. _`session.cache-limiter`: https://www.php.net/manual/en/session.configuration.php#ini.session.cache-limiter +.. _`Microsoft NTLM authentication protocol`: https://docs.microsoft.com/en-us/windows/desktop/secauthn/microsoft-ntlm +.. _`utf-8 modifier`: https://www.php.net/reference.pcre.pattern.modifiers diff --git a/reference/configuration/kernel.rst b/reference/configuration/kernel.rst index b829c5c99dd..5f52cd155e7 100644 --- a/reference/configuration/kernel.rst +++ b/reference/configuration/kernel.rst @@ -1,23 +1,30 @@ .. index:: single: Configuration reference; Kernel class -Configuring in the Kernel (e.g. AppKernel) -========================================== +Configuring in the Kernel +========================= -Some configuration can be done on the kernel class itself (usually called -``app/AppKernel.php``). You can do this by overriding specific methods in +Some configuration can be done on the kernel class itself (located by default at +``src/Kernel.php``). You can do this by overriding specific methods in the parent :class:`Symfony\\Component\\HttpKernel\\Kernel` class. Configuration ------------- * `Charset`_ -* `Kernel Name`_ -* `Root Directory`_ +* `Project Directory`_ * `Cache Directory`_ * `Log Directory`_ * `Container Build Time`_ +In previous Symfony versions there was another configuration option to define +the "kernel name", which is only important when +:doc:`using applications with multiple kernels `. +If you need a unique ID for your kernels use the ``kernel.container_class`` +parameter or the ``Kernel::getContainerClass()`` method. + +.. _configuration-kernel-charset: + Charset ~~~~~~~ @@ -30,10 +37,11 @@ exposed via the ``kernel.charset`` configuration parameter and the To change this value, override the ``getCharset()`` method and return another charset:: - // app/AppKernel.php - + // src/Kernel.php + use Symfony\Component\HttpKernel\Kernel as BaseKernel; // ... - class AppKernel extends Kernel + + class Kernel extends BaseKernel { public function getCharset() { @@ -41,94 +49,49 @@ charset:: } } -Kernel Name -~~~~~~~~~~~ - -**type**: ``string`` **default**: ``app`` (i.e. the directory name holding -the kernel class) - -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``). - -This value is exposed via the ``kernel.name`` configuration parameter and the -:method:`Symfony\\Component\\HttpKernel\\Kernel::getName` method. - -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``. - -Root Directory -~~~~~~~~~~~~~~ - -.. deprecated:: 3.3 - - The ``getRootDir()`` method is deprecated since Symfony 3.3. Use the new - ``getProjectDir()`` method instead. - -**type**: ``string`` **default**: the directory of ``AppKernel`` - -This returns the absolute path of the directory where your kernel class is -located. If you use the Symfony Standard edition, this is the ``app/`` directory -of your project. - -This value is exposed via the ``kernel.root_dir`` configuration parameter and -the :method:`Symfony\\Component\\HttpKernel\\Kernel::getRootDir` method. To -change this setting, override the ``getRootDir()`` method:: - - // app/AppKernel.php - - // ... - class AppKernel extends Kernel - { - // ... - - public function getRootDir() - { - return realpath(parent::getRootDir().'/../'); - } - } +.. _configuration-kernel-project-directory: Project Directory ~~~~~~~~~~~~~~~~~ -.. versionadded:: 3.3 - - The ``getProjectDir()`` method was introduced in Symfony 3.3. - **type**: ``string`` **default**: the directory of the project ``composer.json`` -This returns the absolute path of the root directory of your Symfony project. -It's calculated automatically as the directory where the main ``composer.json`` -file is stored. +This returns the absolute path of the root directory of your Symfony project, +which is used by applications to perform operations with file paths relative to +the project's root directory. -This value is exposed via the ``kernel.project_dir`` configuration parameter and -the :method:`Symfony\\Component\\HttpKernel\\Kernel::getProjectDir` method. To -change this setting, override the ``getProjectDir()`` method to return the right -project directory:: +By default, its value is calculated automatically as the directory where the +main ``composer.json`` file is stored. This value is exposed via the +``kernel.project_dir`` configuration parameter and the +:method:`Symfony\\Component\\HttpKernel\\Kernel::getProjectDir` method. - // app/AppKernel.php +If you don't use Composer, or have moved the ``composer.json`` file location or +have deleted it entirely (for example in the production servers), you can +override the :method:`Symfony\\Component\\HttpKernel\\Kernel::getProjectDir` +method to return the right project directory:: + // src/Kernel.php + use Symfony\Component\HttpKernel\Kernel as BaseKernel; // ... - class AppKernel extends Kernel + + class Kernel extends BaseKernel { // ... - public function getProjectDir() + public function getProjectDir(): string { - return realpath(__DIR__.'/../'); + return \dirname(__DIR__); } } Cache Directory ~~~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``$this->rootDir/cache/$this->environment`` +**type**: ``string`` **default**: ``$this->getProjectDir()/var/cache/$this->environment`` This returns the absolute path of the cache directory of your Symfony project. It's calculated automatically based on the current -:doc:`environment `. +:ref:`environment `. This value is exposed via the ``kernel.cache_dir`` configuration parameter and the :method:`Symfony\\Component\\HttpKernel\\Kernel::getCacheDir` method. To @@ -138,13 +101,13 @@ cache directory. Log Directory ~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``$this->rootDir/logs`` +**type**: ``string`` **default**: ``$this->getProjectDir()/var/log`` This returns the absolute path of the log directory of your Symfony project. It's calculated automatically based on the current -:doc:`environment `. +:ref:`environment `. -This value is exposed via the ``kernel.log_dir`` configuration parameter and +This value is exposed via the ``kernel.logs_dir`` configuration parameter and the :method:`Symfony\\Component\\HttpKernel\\Kernel::getLogDir` method. To change this setting, override the ``getLogDir()`` method to return the right log directory. @@ -179,14 +142,14 @@ achieve a strict reproducible build: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml parameters: # ... kernel.container_build_time: '1234567890' .. code-block:: xml - + setParameter('kernel.container_build_time', '1234567890'); diff --git a/reference/configuration/security.rst b/reference/configuration/security.rst index 99eaa1e5f93..de881abfc3a 100644 --- a/reference/configuration/security.rst +++ b/reference/configuration/security.rst @@ -39,22 +39,21 @@ Some of these options define tens of sub-options and they are explained in separate articles: * `access_control`_ -* `acl`_ * `encoders`_ * `firewalls`_ * `providers`_ * `role_hierarchy`_ -``access_denied_url`` -~~~~~~~~~~~~~~~~~~~~~ +access_denied_url +~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``null`` Defines the URL where the user is redirected after a ``403`` HTTP error (unless you define a custom access deny handler). Example: ``/no-permission`` -``always_authenticate_before_granting`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +always_authenticate_before_granting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``false`` @@ -62,16 +61,16 @@ If ``true``, the user is asked to authenticate before each call to the ``isGranted()`` method in services and controllers or ``is_granted()`` from templates. -``erase_credentials`` -~~~~~~~~~~~~~~~~~~~~~ +erase_credentials +~~~~~~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``true`` If ``true``, the ``eraseCredentials()`` method of the user object is called after authentication. -``hide_user_not_found`` -~~~~~~~~~~~~~~~~~~~~~~~ +hide_user_not_found +~~~~~~~~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``true`` @@ -83,8 +82,8 @@ If ``false``, the exception thrown is of type :class:`Symfony\\Component\\Security\\Core\\Exception\\UsernameNotFoundException` and it includes the given not found username. -``session_fixation_strategy`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +session_fixation_strategy +~~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``SessionAuthenticationStrategy::MIGRATE`` @@ -102,8 +101,8 @@ The possible values of this option are: The entire session is regenerated, so the session ID is updated but all the other session attributes are lost. -``access_control`` ------------------- +access_control +-------------- Defines the security protection of the URLs of your application. It's used for example to trigger the user authentication when trying to access to the backend @@ -111,19 +110,8 @@ and to allow anonymous users to the login form page. This option is explained in detail in :doc:`/security/access_control`. -``acl`` -------- - -This option is used to define `ACL (Access Control List)`_, which allow to -associate a list of permissions to an object. This option is deprecated since -Symfony 3.4 and will be removed in 4.0. - -Instead of using ACLs, Symfony recommends :doc:`security voters `, -which provide the same granular security access without the complication of ACLs. -If you still want to implement ACLs, check out the `Symfony ACL Bundle`_. - -``encoders`` ------------- +encoders +-------- This option defines the algorithm used to *encode* the password of the users. Although Symfony calls it *"password encoding"* for historical reasons, this is @@ -136,28 +124,34 @@ encoding algorithm. Also, each algorithm defines different config options: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... encoders: - # bcrypt encoder with default options - AppBundle\Entity\User: 'bcrypt' + # auto encoder with default options + App\Entity\User: 'auto' - # bcrypt encoder with custom options - AppBundle\Entity\User: - algorithm: 'bcrypt' + # auto encoder with custom options + App\Entity\User: + algorithm: 'auto' cost: 15 - # Argon2i encoder with default options - AppBundle\Entity\User: 'argon2i' + # Sodium encoder with default options + App\Entity\User: 'sodium' + + # Sodium encoder with custom options + App\Entity\User: + algorithm: 'sodium' + memory_cost: 16384 # Amount in KiB. (16384 = 16 MiB) + time_cost: 2 # Number of iterations # MessageDigestPasswordEncoder encoder using SHA512 hashing with default options AppBundle\Entity\User: 'sha512' .. code-block:: xml - + - + - + - + + + + + @@ -196,26 +200,33 @@ encoding algorithm. Also, each algorithm defines different config options: .. code-block:: php - // app/config/security.php - use AppBundle\Entity\User; + // config/packages/security.php + use App\Entity\User; $container->loadFromExtension('security', [ // ... 'encoders' => [ - // bcrypt encoder with default options + // auto encoder with default options User::class => [ - 'algorithm' => 'bcrypt', + 'algorithm' => 'auto', ], - // bcrypt encoder with custom options + // auto encoder with custom options User::class => [ - 'algorithm' => 'bcrypt', + 'algorithm' => 'auto', 'cost' => 15, ], - // Argon2i encoder with default options + // Sodium encoder with default options User::class => [ - 'algorithm' => 'argon2i', + 'algorithm' => 'sodium', + ], + + // Sodium encoder with custom options + User::class => [ + 'algorithm' => 'sodium', + 'memory_cost' => 16384, // Amount in KiB. (16384 = 16 MiB) + 'time_cost' => 2, // Number of iterations ], // MessageDigestPasswordEncoder encoder using SHA512 hashing with default options @@ -231,10 +242,11 @@ encoding algorithm. Also, each algorithm defines different config options: select a different password encoder for each user instance. Read :doc:`this article ` for more details. -.. _reference-security-argon2i: +.. _reference-security-sodium: +.. _using-the-argon2i-password-encoder: -Using the Argon2i Password Encoder -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Using the Sodium Password Encoder +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It uses the `Argon2 key derivation function`_ and it's the encoder recommended by Symfony. Argon2 support was introduced in PHP 7.2, but if you use an earlier @@ -246,16 +258,20 @@ sure to allocate enough space for them to be persisted. Also, passwords include the `cryptographic salt`_ inside them (it's generated automatically for each new password) so you don't have to deal with it. -.. _reference-security-bcrypt: +.. _reference-security-encoder-auto: -Using the Bcrypt Password Encoder +Using the "auto" Password Encoder ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -It uses the `bcrypt password hashing function`_ and it's recommended to use it -when it's not possible to use Argon2i. The encoded passwords are ``60`` -characters long, so make sure to allocate enough space for them to be persisted. -Also, passwords include the `cryptographic salt`_ inside them (it's generated -automatically for each new password) so you don't have to deal with it. +It selects automatically the best possible encoder. Currently, it tries to use +Sodium by default and falls back to the `bcrypt password hashing function`_ if +not possible. In the future, when PHP adds new hashing techniques, it may use +different password hashers. + +It produces encoded passwords with ``60`` characters long, so make sure to +allocate enough space for them to be persisted. Also, passwords include the +`cryptographic salt`_ inside them (it's generated automatically for each new +password) so you don't have to deal with it. Its only configuration option is ``cost``, which is an integer in the range of ``4-31`` (by default, ``13``). Each single increment of the cost **doubles the @@ -269,7 +285,7 @@ used back when they were encoded. .. tip:: - A simple technique to make tests much faster when using bcrypt is to set + A simple technique to make tests much faster when using BCrypt is to set the cost to ``4``, which is the minimum value allowed, in the ``test`` environment configuration. @@ -279,11 +295,11 @@ Using the PBKDF2 Encoder ~~~~~~~~~~~~~~~~~~~~~~~~ Using the `PBKDF2`_ encoder is no longer recommended since PHP added support for -Argon2i and bcrypt. Legacy application still using it are encouraged to upgrade +Sodium and BCrypt. Legacy application still using it are encouraged to upgrade to those newer encoding algorithms. -``firewalls`` -------------- +firewalls +--------- This is arguably the most important option of the security config file. It defines the authentication mechanism used for each URL (or URL pattern) of your @@ -293,7 +309,7 @@ application: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... firewalls: @@ -307,7 +323,7 @@ application: .. code-block:: xml - + loadFromExtension('security', [ @@ -355,7 +371,7 @@ depend on the authentication mechanism, which can be any of these: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... firewalls: @@ -393,8 +409,8 @@ 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 :doc:`/security/form_login`. -``login_path`` -.............. +login_path +.......... **type**: ``string`` **default**: ``/login`` @@ -402,12 +418,11 @@ This is the route or path that the user will be redirected to (unless ``use_forw 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, unauthenticated user, else -you may create a redirect loop. For details, see -":ref:`Avoid Common Pitfalls `". +This path **must** be accessible by a normal, un-authenticated user, else +you may create a redirect loop. -``check_path`` -.............. +check_path +.......... **type**: ``string`` **default**: ``/login_check`` @@ -418,16 +433,16 @@ URL and process the submitted login credentials. Be sure that this URL is covered by your main firewall (i.e. don't create a separate firewall just for ``check_path`` URL). -``use_forward`` -............... +use_forward +........... **type**: ``boolean`` **default**: ``false`` If you'd like the user to be forwarded to the login form instead of being redirected, set this option to ``true``. -``username_parameter`` -...................... +username_parameter +.................. **type**: ``string`` **default**: ``_username`` @@ -435,8 +450,8 @@ This is the field name that you should give to the username field of your login form. When you submit the form to ``check_path``, the security system will look for a POST parameter with this name. -``password_parameter`` -...................... +password_parameter +.................. **type**: ``string`` **default**: ``_password`` @@ -444,8 +459,8 @@ This is the field name that you should give to the password field of your login form. When you submit the form to ``check_path``, the security system will look for a POST parameter with this name. -``post_only`` -............. +post_only +......... **type**: ``boolean`` **default**: ``true`` @@ -455,32 +470,32 @@ request to the ``check_path`` URL. **Options Related to Redirecting after Login** -``always_use_default_target_path`` -.................................. +always_use_default_target_path +.............................. **type**: ``boolean`` **default**: ``false`` If ``true``, users are always redirected to the default target path regardless of the previous URL that was stored in the session. -``default_target_path`` -........................ +default_target_path +.................... **type**: ``string`` **default**: ``/`` The page users are redirected to when there is no previous page stored in the session (for example, when the users browse the login page directly). -``target_path_parameter`` -......................... +target_path_parameter +..................... **type**: ``string`` **default**: ``_target_path`` When using a login form, if you include an HTML element to set the target path, this option lets you change the name of the HTML element itself. -``use_referer`` -............... +use_referer +........... **type**: ``boolean`` **default**: ``false`` @@ -496,8 +511,8 @@ redirected to the ``default_target_path`` to avoid a redirection loop. **Options Related to Logout Configuration** -``invalidate_session`` -~~~~~~~~~~~~~~~~~~~~~~ +invalidate_session +~~~~~~~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``true`` @@ -509,32 +524,38 @@ The ``invalidate_session`` option allows to redefine this behavior. Set this option to ``false`` in every firewall and the user will only be logged out from the current firewall and not the other ones. -``logout_on_user_change`` -~~~~~~~~~~~~~~~~~~~~~~~~~ +success_handler +~~~~~~~~~~~~~~~ -**type**: ``boolean`` **default**: ``false`` +**type**: ``string`` **default**: ``'security.logout.success_handler'`` -.. versionadded:: 3.4 +The service ID used for handling a successful logout. The service must implement +:class:`Symfony\\Component\\Security\\Http\\Logout\\LogoutSuccessHandlerInterface`. - The ``logout_on_user_change`` option was introduced in Symfony 3.4. +.. _reference-security-logout-csrf: -If ``true`` this option makes Symfony to trigger a logout when the user has -changed. Not doing that is deprecated, so this option should be set to ``true`` -to avoid getting deprecation messages. +csrf_parameter +~~~~~~~~~~~~~~ -The user is considered to have changed when the user class implements -:class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface` and the -``isEqualTo()`` method returns ``false``. Also, when any of the properties -required by the :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface` -(like the username, password or salt) changes. +**type**: ``string`` **default**: ``'_csrf_token'`` -``success_handler`` -~~~~~~~~~~~~~~~~~~~ +The name of the parameter that stores the CSRF token value. -**type**: ``string`` **default**: ``'security.logout.success_handler'`` +csrf_token_generator +~~~~~~~~~~~~~~~~~~~~ -The service ID used for handling a successful logout. The service must implement -:class:`Symfony\\Component\\Security\\Http\\Logout\\LogoutSuccessHandlerInterface`. +**type**: ``string`` **default**: ``null`` + +The ``id`` of the service used to generate the CSRF tokens. Symfony provides a +default service whose ID is ``security.csrf.token_manager``. + +csrf_token_id +~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``'logout'`` + +An arbitrary string used to generate the token value (and check its validity +afterwards). .. _reference-security-ldap: @@ -542,7 +563,7 @@ LDAP Authentication ~~~~~~~~~~~~~~~~~~~ There are several options for connecting against an LDAP server, -using the ``form_login_ldap`` and ``http_basic_ldap`` authentication +using the ``form_login_ldap``, ``http_basic_ldap`` and ``json_login_ldap`` authentication providers or the ``ldap`` user provider. For even more details, see :doc:`/security/ldap`. @@ -550,22 +571,22 @@ For even more details, see :doc:`/security/ldap`. **Authentication** You can authenticate to an LDAP server using the LDAP variants of the -``form_login`` and ``http_basic`` authentication providers. Simply use -``form_login_ldap`` and ``http_basic_ldap``, which will attempt to -``bind`` against an LDAP server instead of using password comparison. +``form_login``, ``http_basic`` and ``json_login`` authentication providers. Use +``form_login_ldap``, ``http_basic_ldap`` and ``json_login_ldap``, which will +attempt to ``bind`` against an LDAP server instead of using password comparison. Both authentication providers have the same arguments as their normal counterparts, with the addition of two configuration keys: -``service`` -........... +service +....... **type**: ``string`` **default**: ``ldap`` This is the name of your configured LDAP client. -``dn_string`` -............. +dn_string +......... **type**: ``string`` **default**: ``{username}`` @@ -574,8 +595,8 @@ placeholder will be replaced with the user-provided value (their login). Depending on your LDAP server's configuration, you may need to override this value. -``query_string`` -................ +query_string +............ **type**: ``string`` **default**: ``null`` @@ -587,82 +608,10 @@ statically using the ``dn_string`` config option. **User provider** -Users will still be fetched from the configured user provider. If you -wish to fetch your users from an LDAP server, you will need to use the -``ldap`` user provider, in addition to one of the two authentication -providers (``form_login_ldap`` or ``http_basic_ldap``). - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - providers: - my_ldap_users: - ldap: - service: ldap - base_dn: 'dc=symfony,dc=com' - search_dn: '%ldap.search_dn%' - search_password: '%ldap.search_password%' - default_roles: '' - uid_key: 'uid' - filter: '(&({uid_key}={username})(objectclass=person)(ou=Users))' - -HTTP-Digest Authentication -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 3.4 - - HTTP-Digest Authentication was deprecated in Symfony 3.4 and will be - removed in Symfony 4.0. - -To use HTTP-Digest authentication you need to provide a realm and a secret: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - somename: - http_digest: - secret: '%secret%' - realm: 'secure-api' - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'somename' => [ - 'http_digest' => [ - 'secret' => '%secret%', - 'realm' => 'secure-api', - ], - ], - ], - ]); +Users will still be fetched from the configured user provider. If you wish to +fetch your users from an LDAP server, you will need to use the +:doc:`LDAP User Provider ` and any of these authentication +providers: ``form_login_ldap`` or ``http_basic_ldap`` or ``json_login_ldap``. .. _reference-security-firewall-context: @@ -684,7 +633,7 @@ multiple firewalls, the "context" could actually be shared: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -698,7 +647,7 @@ multiple firewalls, the "context" could actually be shared: .. code-block:: xml - + loadFromExtension('security', [ 'firewalls' => [ 'somename' => [ @@ -748,20 +697,20 @@ a ``user_checker`` option to define the service used to perform those checks. Learn more about user checkers in :doc:`/security/user_checkers`. -``providers`` -------------- +providers +--------- This options defines how the application users are loaded (from a database, an LDAP server, a configuration file, etc.) Read the following articles to learn more about each of those providers: -* :doc:`Load users from a database ` -* :doc:`Load users from an LDAP server ` -* :ref:`Load users from a configuration file ` -* :doc:`Create your own user provider ` +* :ref:`Load users from a database ` +* :ref:`Load users from an LDAP server ` +* :ref:`Load users from a configuration file ` +* :ref:`Create your own user provider ` -``role_hierarchy`` ------------------- +role_hierarchy +-------------- Instead of associating many roles to users, this option allows you to define role inheritance rules by creating a role hierarchy, as explained in @@ -770,8 +719,6 @@ role inheritance rules by creating a role hierarchy, as explained in .. _`PBKDF2`: https://en.wikipedia.org/wiki/PBKDF2 .. _`libsodium`: https://pecl.php.net/package/libsodium .. _`Session Fixation`: https://www.owasp.org/index.php/Session_fixation -.. _`ACL (Access Control List)`: https://en.wikipedia.org/wiki/Access_control_list -.. _`Symfony ACL Bundle`: https://github.com/symfony/acl-bundle .. _`Argon2 key derivation function`: https://en.wikipedia.org/wiki/Argon2 .. _`bcrypt password hashing function`: https://en.wikipedia.org/wiki/Bcrypt .. _`cryptographic salt`: https://en.wikipedia.org/wiki/Salt_(cryptography) diff --git a/reference/configuration/swiftmailer.rst b/reference/configuration/swiftmailer.rst index dd6033438f2..42bea0dc659 100644 --- a/reference/configuration/swiftmailer.rst +++ b/reference/configuration/swiftmailer.rst @@ -4,7 +4,7 @@ Mailer Configuration Reference (SwiftmailerBundle) ================================================== -The SwiftmailerBundle integrates the Swift Mailer library in Symfony applications +The SwiftmailerBundle integrates the Swiftmailer library in Symfony applications to :doc:`send emails `. All these options are configured under the ``swiftmailer`` key in your application configuration. @@ -55,112 +55,112 @@ Configuration * `url`_ * `username`_ -``url`` -~~~~~~~ +url +~~~ **type**: ``string`` -The entire Swift Mailer configuration using a DSN-like URL format. +The entire SwiftMailer configuration using a DSN-like URL format. Example: ``smtp://user:pass@host:port/?timeout=60&encryption=ssl&auth_mode=login&...`` -``transport`` -~~~~~~~~~~~~~ +transport +~~~~~~~~~ **type**: ``string`` **default**: ``smtp`` The exact transport method to use to deliver emails. Valid values are: -* ``smtp`` -* ``gmail`` (see :doc:`/email/gmail`) -* ``mail`` (deprecated in Swift Mailer since version 5.4.5) -* ``sendmail`` -* ``null`` (same as setting `disable_delivery`_ to ``true``) +* smtp +* gmail (see :ref:`email-using-gmail`) +* mail (deprecated in SwiftMailer since version 5.4.5) +* sendmail +* null (same as setting `disable_delivery`_ to ``true``) -``username`` -~~~~~~~~~~~~ +username +~~~~~~~~ **type**: ``string`` The username when using ``smtp`` as the transport. -``password`` -~~~~~~~~~~~~ +password +~~~~~~~~ **type**: ``string`` The password when using ``smtp`` as the transport. -``command`` -~~~~~~~~~~~~ +command +~~~~~~~~ **type**: ``string`` **default**: ``/usr/sbin/sendmail -bs`` Command to be executed by ``sendmail`` transport. -``host`` -~~~~~~~~ +host +~~~~ **type**: ``string`` **default**: ``localhost`` The host to connect to when using ``smtp`` as the transport. -``port`` -~~~~~~~~ +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. -``timeout`` -~~~~~~~~~~~ +timeout +~~~~~~~ **type**: ``integer`` The timeout in seconds when using ``smtp`` as the transport. -``source_ip`` -~~~~~~~~~~~~~ +source_ip +~~~~~~~~~ **type**: ``string`` The source IP address when using ``smtp`` as the transport. -``local_domain`` -~~~~~~~~~~~~~~~~ +local_domain +~~~~~~~~~~~~ + +**type**: ``string`` .. versionadded:: 2.4.0 The ``local_domain`` option was introduced in SwiftMailerBundle 2.4.0. -**type**: ``string`` - The domain name to use in ``HELO`` command. -``encryption`` -~~~~~~~~~~~~~~ +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`` -~~~~~~~~~~~~~ +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`` -~~~~~~~~~ +spool +~~~~~ -For details on email spooling, see :doc:`/email/spool`. +For details on email spooling, see :doc:`/mailer`. -``type`` -........ +type +.... **type**: ``string`` **default**: ``file`` @@ -168,16 +168,16 @@ The method used to store spooled messages. Valid values are ``memory`` and ``file``. A custom spool should be possible by creating a service called ``swiftmailer.spool.myspool`` and setting this value to ``myspool``. -``path`` -........ +path +.... **type**: ``string`` **default**: ``%kernel.cache_dir%/swiftmailer/spool`` When using the ``file`` spool, this is the path where the spooled messages will be stored. -``sender_address`` -~~~~~~~~~~~~~~~~~~ +sender_address +~~~~~~~~~~~~~~ **type**: ``string`` @@ -185,19 +185,19 @@ 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`` -~~~~~~~~~~~~~ +antiflood +~~~~~~~~~ -``threshold`` -............. +threshold +......... **type**: ``integer`` **default**: ``99`` Used with ``Swift_Plugins_AntiFloodPlugin``. This is the number of emails to send before restarting the transport. -``sleep`` -......... +sleep +..... **type**: ``integer`` **default**: ``0`` @@ -206,8 +206,8 @@ to sleep for during a transport restart. .. _delivery-address: -``delivery_addresses`` -~~~~~~~~~~~~~~~~~~~~~~ +delivery_addresses +~~~~~~~~~~~~~~~~~~ **type**: ``array`` @@ -215,16 +215,17 @@ to sleep for during a transport restart. In previous versions, this option was called ``delivery_address``. -If set, all email messages will be sent to these addresses 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 one or more some specific accounts. +If set, all email messages will be sent to these addresses instead of being sent +to their actual recipients. This is often useful when developing. For example, +by setting this in the ``config/packages/dev/swiftmailer.yaml`` file, you can +guarantee that all emails sent during development go to one or more some +specific accounts. This uses ``Swift_Plugins_RedirectingPlugin``. Original recipients are available on the ``X-Swift-To``, ``X-Swift-Cc`` and ``X-Swift-Bcc`` headers. -``delivery_whitelist`` -~~~~~~~~~~~~~~~~~~~~~~ +delivery_whitelist +~~~~~~~~~~~~~~~~~~ **type**: ``array`` @@ -234,16 +235,16 @@ of these patterns will be delivered like normal, as well as being sent to :ref:`How to Work with Emails during Development ` article. -``disable_delivery`` -~~~~~~~~~~~~~~~~~~~~ +disable_delivery +~~~~~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``false`` If true, the ``transport`` will automatically be set to ``null`` and no emails will actually be delivered. -``logging`` -~~~~~~~~~~~ +logging +~~~~~~~ **type**: ``boolean`` **default**: ``%kernel.debug%`` @@ -252,11 +253,10 @@ the information will be available in the profiler. .. tip:: - The following options can be set via environment variables using the - ``%env()%`` syntax: ``url``, ``transport``, ``username``, ``password``, - ``host``, ``port``, ``timeout``, ``source_ip``, ``local_domain``, - ``encryption``, ``auth_mode``. - For details, see the :doc:`/configuration/external_parameters` article. + The following options can be set via environment variables: ``url``, + ``transport``, ``username``, ``password``, ``host``, ``port``, ``timeout``, + ``source_ip``, ``local_domain``, ``encryption``, ``auth_mode``. For details, + see: :ref:`config-env-vars`. Using Multiple Mailers ---------------------- @@ -307,7 +307,7 @@ key (the default mailer is identified by the ``default_mailer`` option): ], ]); -Each mailer is registered as a service:: +Each mailer is registered automatically as a service with these IDs:: // ... @@ -325,3 +325,71 @@ Each mailer is registered as a service:: When configuring multiple mailers, options must be placed under the appropriate mailer key of the configuration instead of directly under the ``swiftmailer`` key. + +When using :ref:`autowiring ` only the default mailer is +injected when type-hinting some argument with the ``\Swift_Mailer`` class. If +you need to inject a different mailer in some service, use any of these +alternatives based on the :ref:`service binding ` feature: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + _defaults: + bind: + # this injects the second mailer when type-hinting constructor arguments with \Swift_Mailer + \Swift_Mailer: '@swiftmailer.mailer.second_mailer' + # this injects the second mailer when a service constructor argument is called $specialMailer + $specialMailer: '@swiftmailer.mailer.second_mailer' + + App\Some\Service: + # this injects the second mailer only for this argument of this service + $differentMailer: '@swiftmailer.mailer.second_mailer' + + # ... + + .. code-block:: xml + + + + + + + + + @swiftmailer.mailer.second_mailer + + @swiftmailer.mailer.second_mailer + + + + + @swiftmailer.mailer.second_mailer + + + + + + + .. code-block:: php + + // config/services.php + use App\Some\Service; + use Psr\Log\LoggerInterface; + use Symfony\Component\DependencyInjection\Reference; + + + $container->register(Service::class) + ->setPublic(true) + ->setBindings([ + // this injects the second mailer when this service type-hints constructor arguments with \Swift_Mailer + \Swift_Mailer::class => '@swiftmailer.mailer.second_mailer', + // this injects the second mailer when this service has a constructor argument called $specialMailer + '$specialMailer' => '@swiftmailer.mailer.second_mailer', + ]) + ; diff --git a/reference/configuration/twig.rst b/reference/configuration/twig.rst index 5c18bef30c4..7f83f0929a8 100644 --- a/reference/configuration/twig.rst +++ b/reference/configuration/twig.rst @@ -5,8 +5,8 @@ Twig Configuration Reference (TwigBundle) ========================================= The TwigBundle integrates the Twig library in Symfony applications to -:doc:`render templates `. All these options are configured under -the ``twig`` key in your application configuration. +:ref:`render templates `. All these options are configured +under the ``twig`` key in your application configuration. .. code-block:: terminal @@ -41,7 +41,9 @@ Configuration * `timezone`_ * `debug`_ -* `exception_controller`_ +* `default_path`_ +* `form_themes`_ +* `globals`_ * `number_format`_ * `decimals`_ @@ -52,8 +54,8 @@ Configuration * `paths`_ * `strict_variables`_ -``auto_reload`` -~~~~~~~~~~~~~~~ +auto_reload +~~~~~~~~~~~ **type**: ``boolean`` **default**: ``%kernel.debug%`` @@ -61,8 +63,10 @@ If ``true``, whenever a template is rendered, Symfony checks first if its source code has changed since it was compiled. If it has changed, the template is compiled again automatically. -``autoescape`` -~~~~~~~~~~~~~~ +.. _config-twig-autoescape: + +autoescape +~~~~~~~~~~ **type**: ``boolean`` or ``string`` **default**: ``'name'`` @@ -72,7 +76,7 @@ individually in the templates). .. caution:: Setting this option to ``false`` is dangerous and it will make your - application vulnerable to XSS exploits because most third-party bundles + application vulnerable to `XSS attacks`_ because most third-party bundles assume that auto-escaping is enabled and they don't escape contents themselves. @@ -87,8 +91,8 @@ templates and ``js`` for ``*.js.html`` templates). See `autoescape_service`_ and `autoescape_service_method`_ to define your own escaping strategy. -``autoescape_service`` -~~~~~~~~~~~~~~~~~~~~~~ +autoescape_service +~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``null`` @@ -100,16 +104,16 @@ for HTML and the contents of ``*.js.twig`` are escaped for JavaScript. This option allows to define the Symfony service which will be used to determine the default escaping applied to the template. -``autoescape_service_method`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +autoescape_service_method +~~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``null`` If ``autoescape_service`` option is defined, then this option defines the method called to determine the default escaping applied to the template. -``base_template_class`` -~~~~~~~~~~~~~~~~~~~~~~~ +base_template_class +~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``'Twig\Template'`` @@ -118,8 +122,8 @@ contents. This option defines the base class from which all the template classes extend. Using a custom base template is discouraged because it will make your application harder to maintain. -``cache`` -~~~~~~~~~ +cache +~~~~~ **type**: ``string`` | ``false`` **default**: ``'%kernel.cache_dir%/twig'`` @@ -132,86 +136,146 @@ is not recommended; not even in the ``dev`` environment, because the ``auto_reload`` option ensures that cached templates which have changed get compiled again. -``charset`` -~~~~~~~~~~~ +charset +~~~~~~~ **type**: ``string`` **default**: ``'%kernel.charset%'`` -The charset used by the template files. In the Symfony Standard edition this -defaults to the ``UTF-8`` charset. +The charset used by the template files. By default it's the same as the value of +the :ref:`kernel.charset container parameter `, +which is ``UTF-8`` by default in Symfony applications. -``date`` -~~~~~~~~ +date +~~~~ These options define the default values used by the ``date`` filter to format date and time values. They are useful to avoid passing the same arguments on every ``date`` filter call. -``format`` -.......... +format +...... **type**: ``string`` **default**: ``F j, Y H:i`` The format used by the ``date`` filter to display values when no specific format is passed as argument. -``interval_format`` -................... +interval_format +............... **type**: ``string`` **default**: ``%d days`` The format used by the ``date`` filter to display ``DateInterval`` instances when no specific format is passed as argument. -``timezone`` -............ +timezone +........ **type**: ``string`` **default**: (the value returned by ``date_default_timezone_get()``) The timezone used when formatting date values with the ``date`` filter and no specific timezone is passed as argument. -``debug`` -~~~~~~~~~ +debug +~~~~~ **type**: ``boolean`` **default**: ``%kernel.debug%`` If ``true``, the compiled templates include a ``__toString()`` method that can be used to display their nodes. -.. _config-twig-exception-controller: +.. _config-twig-default-path: + +default_path +~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``'%kernel.project_dir%/templates'`` + +The path to the directory where Symfony will look for the application Twig +templates by default. If you store the templates in more than one directory, use +the :ref:`paths ` option too. + +.. _config-twig-form-themes: + +form_themes +~~~~~~~~~~~ + +**type**: ``array`` of ``string`` **default**: ``['form_div_layout.html.twig']`` + +Defines one or more :doc:`form themes ` which are applied to +all the forms of the application: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/twig.yaml + twig: + form_themes: ['bootstrap_4_layout.html.twig', 'form/my_theme.html.twig'] + # ... + + .. code-block:: xml + + + + + + + bootstrap_4_layout.html.twig + form/my_theme.html.twig + + + + + .. code-block:: php -``exception_controller`` -~~~~~~~~~~~~~~~~~~~~~~~~ + // config/packages/twig.php + $container->loadFromExtension('twig', [ + 'form_themes' => [ + 'bootstrap_4_layout.html.twig', + 'form/my_theme.html.twig', + ], + // ... + ]); + +The order in which themes are defined is important because each theme overrides +all the previous one. When rendering a form field whose block is not defined in +the form theme, Symfony falls back to the previous themes until the first one. + +These global themes are applied to all forms, even those which use the +:ref:`form_theme Twig tag `, but you can +:ref:`disable global themes for specific forms `. -**type**: ``string`` **default**: ``twig.controller.exception:showAction`` +globals +~~~~~~~ -This is the controller that is activated after an exception is thrown anywhere -in your application. The default controller -(:class:`Symfony\\Bundle\\TwigBundle\\Controller\\ExceptionController`) -is what's responsible for rendering specific templates under different error -conditions (see :doc:`/controller/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 :ref:`dic-tags-kernel-event-listener`). +**type**: ``array`` **default**: ``[]`` -``number_format`` -~~~~~~~~~~~~~~~~~ +It defines the global variables injected automatically into all Twig templates. +Learn more about :doc:`Twig global variables `. + +number_format +~~~~~~~~~~~~~ These options define the default values used by the ``number_format`` filter to format numeric values. They are useful to avoid passing the same arguments on every ``number_format`` filter call. -``decimals`` -............ +decimals +........ **type**: ``integer`` **default**: ``0`` The number of decimals used to format numeric values when no specific number is passed as argument to the ``number_format`` filter. -``decimal_point`` -................. +decimal_point +............. **type**: ``string`` **default**: ``.`` @@ -219,16 +283,16 @@ The character used to separate the decimals from the integer part of numeric values when no specific character is passed as argument to the ``number_format`` filter. -``thousands_separator`` -....................... +thousands_separator +................... **type**: ``string`` **default**: ``,`` The character used to separate every group of thousands in numeric values when no specific character is passed as argument to the ``number_format`` filter. -``optimizations`` -~~~~~~~~~~~~~~~~~ +optimizations +~~~~~~~~~~~~~ **type**: ``int`` **default**: ``-1`` @@ -243,93 +307,30 @@ on. Set it to ``0`` to disable all the optimizations. You can even enable or disable these optimizations selectively, as explained in the Twig documentation about `the optimizer extension`_. -``default_path`` -~~~~~~~~~~~~~~~~ - -**type**: ``string`` **default**: ``'%kernel.project_dir%/templates'`` - -.. versionadded:: 3.4 - - The ``default_path`` option was introduced in Symfony 3.4. - -The default directory where Symfony will look for Twig templates. - .. _config-twig-paths: -``paths`` -~~~~~~~~~ +paths +~~~~~ **type**: ``array`` **default**: ``null`` -This option defines the directories where Symfony will look for Twig templates -in addition to the default locations. Symfony looks for the templates in the -following order: - -#. The directories defined in this option; -#. The ``Resources/views/`` directories of the bundles used in the application; -#. The ``src/Resources/views/`` directory of the application; -#. The directory defined in the ``default_path`` option. - -The values of the ``paths`` option are defined as ``key: value`` pairs where the -``value`` part can be ``null``. For example: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - twig: - # ... - paths: - '%kernel.project_dir%/vendor/acme/foo-bar/templates': ~ - - .. code-block:: xml - - - - - - - %kernel.project_dir%/vendor/acme/foo-bar/templates - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('twig', [ - // ... - 'paths' => [ - '%kernel.project_dir%/vendor/acme/foo-bar/templates' => null, - ], - ]); - -The directories defined in the ``paths`` option have more priority than the -default directories defined by Symfony. In the above example, if the template -exists in the ``acme/foo-bar/templates/`` directory inside your application's -``vendor/``, it will be used by Symfony. - -If you provide a value for any path, Symfony will consider it the Twig namespace -for that directory: +Defines the directories where application templates are stored in addition to +the directory defined in the :ref:`default_path option `: .. configuration-block:: .. code-block:: yaml - # app/config/config.yml + # config/packages/twig.yaml twig: # ... paths: - '%kernel.project_dir%/vendor/acme/foo-bar/templates': 'foo_bar' + 'email/default/templates': ~ + 'backend/templates': 'admin' .. code-block:: xml - + - %kernel.project_dir%/vendor/acme/foo-bar/templates + email/default/templates + backend/templates .. code-block:: php - # app/config/config.php + // config/packages/twig.php $container->loadFromExtension('twig', [ // ... 'paths' => [ - '%kernel.project_dir%/vendor/acme/foo-bar/templates' => 'foo_bar', + 'email/default/templates' => null, + 'backend/templates' => 'admin', ], ]); -This option is useful to not mess with the default template directories defined -by Symfony. Besides, it simplifies how you refer to those templates: - -.. code-block:: text +Read more about :ref:`template directories and namespaces `. - @foo_bar/template_name.html.twig - -``strict_variables`` -~~~~~~~~~~~~~~~~~~~~ +strict_variables +~~~~~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``false`` @@ -370,3 +368,4 @@ attribute or method doesn't exist. If set to ``false`` these errors are ignored and the non-existing values are replaced by ``null``. .. _`the optimizer extension`: https://twig.symfony.com/doc/2.x/api.html#optimizer-extension +.. _`XSS attacks`: https://en.wikipedia.org/wiki/Cross-site_scripting diff --git a/reference/configuration/web_profiler.rst b/reference/configuration/web_profiler.rst index 8772bb225bf..c97280f6f32 100644 --- a/reference/configuration/web_profiler.rst +++ b/reference/configuration/web_profiler.rst @@ -4,10 +4,10 @@ Profiler Configuration Reference (WebProfilerBundle) ==================================================== -The WebProfilerBundle provides detailed technical information about each request -execution and displays it in both the web debug toolbar and the -:doc:`profiler `. All these options are configured under the -``web_profiler`` key in your application configuration. +The WebProfilerBundle is a **development tool** that provides detailed technical +information about each request execution and displays it in both the web debug +toolbar and the :doc:`profiler `. All these options are configured +under the ``web_profiler`` key in your application configuration. .. code-block:: terminal @@ -34,34 +34,22 @@ Configuration * `excluded_ajax_paths`_ * `intercept_redirects`_ -* `position`_ * `toolbar`_ -* `verbose`_ -``toolbar`` -~~~~~~~~~~~ +excluded_ajax_paths +~~~~~~~~~~~~~~~~~~~ -**type**: ``boolean`` **default**: ``false`` - -It enables and disables the toolbar entirely. Usually you set this to ``true`` -in the ``dev`` and ``test`` environments and to ``false`` in the ``prod`` -environment. - -``position`` -~~~~~~~~~~~~ +**type**: ``string`` **default**: ``'^/((index|app(_[\w]+)?)\.php/)?_wdt'`` -.. deprecated:: 3.4 - - The ``position`` option was deprecated in Symfony 3.4 and will be removed - in Symfony 4.0, where the toolbar is always displayed in the ``bottom`` position. - -**type**: ``string`` **default**: ``bottom`` +When the toolbar logs AJAX requests, it matches their URLs against this regular +expression. If the URL matches, the request is not displayed in the toolbar. This +is useful when the application makes lots of AJAX requests, or if they are heavy +and you want to exclude some of them. -It defines the location of the browser window where the toolbar is displayed. -the only allowed values are ``bottom`` and ``top``. +.. _intercept_redirects: -``intercept_redirects`` -~~~~~~~~~~~~~~~~~~~~~~~ +intercept_redirects +~~~~~~~~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``false`` @@ -74,20 +62,11 @@ redirection and shows you the URL which is going to redirect to, its toolbar, and its profiler. Once you've inspected the toolbar/profiler data, you can click on the given link to perform the redirect. -``excluded_ajax_paths`` -~~~~~~~~~~~~~~~~~~~~~~~ +toolbar +~~~~~~~ -**type**: ``string`` **default**: ``'^/(app(_[\\w]+)?\\.php/)?_wdt'`` - -When the toolbar logs AJAX requests, it matches their URLs against this regular -expression. If the URL matches, the request is not displayed in the toolbar. This -is useful when the application makes lots of AJAX requests, or if they are heavy -and you want to exclude some of them. - -``verbose`` -~~~~~~~~~~~ - -**type**: ``boolean`` **default**: ``true`` +**type**: ``boolean`` **default**: ``false`` -This option is **deprecated** and has no effect on the toolbar or the profiler, -so you can safely remove it from your configuration. +It enables and disables the toolbar entirely. Usually you set this to ``true`` +in the ``dev`` and ``test`` environments and to ``false`` in the ``prod`` +environment. diff --git a/reference/constraints.rst b/reference/constraints.rst index 1ba825e2536..317a836e396 100644 --- a/reference/constraints.rst +++ b/reference/constraints.rst @@ -19,6 +19,7 @@ Validation Constraints Reference constraints/Regex constraints/Ip constraints/Uuid + constraints/Json constraints/EqualTo constraints/NotEqualTo @@ -29,10 +30,18 @@ Validation Constraints Reference constraints/GreaterThan constraints/GreaterThanOrEqual constraints/Range + constraints/DivisibleBy + constraints/Unique + + constraints/Positive + constraints/PositiveOrZero + constraints/Negative + constraints/NegativeOrZero constraints/Date constraints/DateTime constraints/Time + constraints/Timezone constraints/Choice constraints/Collection @@ -57,6 +66,7 @@ Validation Constraints Reference constraints/Expression constraints/All constraints/UserPassword + constraints/NotCompromisedPassword constraints/Valid constraints/Traverse diff --git a/reference/constraints/All.rst b/reference/constraints/All.rst index 418762b214c..1577a07ec4d 100644 --- a/reference/constraints/All.rst +++ b/reference/constraints/All.rst @@ -4,17 +4,14 @@ 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 | :ref:`property or method ` | -+----------------+-------------------------------------------------------------------+ -| Options | - `constraints`_ | -| | - `groups`_ | -| | - `payload`_ | -+----------------+-------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Validator\\Constraints\\All` | -+----------------+-------------------------------------------------------------------+ -| Validator | :class:`Symfony\\Component\\Validator\\Constraints\\AllValidator` | -+----------------+-------------------------------------------------------------------+ +========== =================================================================== +Applies to :ref:`property or method ` +Options - `constraints`_ + - `groups`_ + - `payload`_ +Class :class:`Symfony\\Component\\Validator\\Constraints\\All` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\AllValidator` +========== =================================================================== Basic Usage ----------- @@ -26,8 +23,8 @@ entry in that array: .. code-block:: php-annotations - // src/AppBundle/Entity/User.php - namespace AppBundle\Entity; + // src/Entity/User.php + namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; @@ -44,8 +41,8 @@ entry in that array: .. code-block:: yaml - # src/AppBundle/Resources/config/validation.yml - AppBundle\Entity\User: + # config/validator/validation.yaml + App\Entity\User: properties: favoriteColors: - All: @@ -55,13 +52,13 @@ entry in that array: .. code-block:: xml - + - + .. code-block:: php - use AppBundle\Lock\MysqlLock; - use AppBundle\Lock\PostgresqlLock; - use AppBundle\Lock\SqliteLock; + use App\Lock\MysqlLock; + use App\Lock\PostgresqlLock; + use App\Lock\SqliteLock; $container->register('app.mysql_lock', MysqlLock::class)->setPublic(false); $container->register('app.postgresql_lock', PostgresqlLock::class)->setPublic(false); @@ -314,11 +132,11 @@ the generic ``app.lock`` service can be defined as follows: + class="App\Lock\MysqlLock"/> + class="App\Lock\PostgresqlLock"/> + class="App\Lock\SqliteLock"/> @@ -328,9 +146,9 @@ the generic ``app.lock`` service can be defined as follows: .. code-block:: php - use AppBundle\Lock\MysqlLock; - use AppBundle\Lock\PostgresqlLock; - use AppBundle\Lock\SqliteLock; + use App\Lock\MysqlLock; + use App\Lock\PostgresqlLock; + use App\Lock\SqliteLock; $container->register('app.mysql_lock', MysqlLock::class)->setPublic(false); $container->register('app.postgresql_lock', PostgresqlLock::class)->setPublic(false); @@ -355,20 +173,16 @@ wrapping their names with ``%`` characters). You need to manually add the ``Symfony\Component\DependencyInjection\Compiler\AutoAliasServicePass`` compiler pass to the container for this feature to work. -``console.command`` -------------------- +console.command +--------------- **Purpose**: Add a command to the application For details on registering your own commands in the service container, read :doc:`/console/commands_as_services`. -``container.hot_path`` ----------------------- - -.. versionadded:: 3.4 - - The ``container.hot_path`` tag was introduced in Symfony 3.4. +container.hot_path +------------------ **Purpose**: Add to list of always needed services @@ -384,8 +198,8 @@ for services and their class hierarchy. The result is as significant performance Use this tag with great caution, you have to be sure that the tagged service is always used. -``controller.argument_value_resolver`` --------------------------------------- +controller.argument_value_resolver +---------------------------------- **Purpose**: Register a value resolver for controller arguments such as ``Request`` @@ -394,42 +208,42 @@ Value resolvers implement the and are used to resolve argument values for controllers as described here: :doc:`/controller/argument_value_resolver`. -``data_collector`` ------------------- +data_collector +-------------- **Purpose**: Create a class that collects custom data for the profiler For details on creating your own custom data collection, read the :doc:`/profiler/data_collector` article. -``doctrine.event_listener`` ---------------------------- +doctrine.event_listener +----------------------- **Purpose**: Add a Doctrine event listener For details on creating Doctrine event listeners, read the -:doc:`/doctrine/event_listeners_subscribers` article. +:doc:`Doctrine events ` article. -``doctrine.event_subscriber`` ------------------------------ +doctrine.event_subscriber +------------------------- **Purpose**: Add a Doctrine event subscriber For details on creating Doctrine event subscribers, read the -:doc:`/doctrine/event_listeners_subscribers` article. +:doc:`Doctrine events ` article. .. _dic-tags-form-type: -``form.type`` -------------- +form.type +--------- **Purpose**: Create a custom form field type For details on creating your own custom form type, read the :doc:`/form/create_custom_field_type` article. -``form.type_extension`` ------------------------ +form.type_extension +------------------- **Purpose**: Create a custom "form extension" @@ -438,12 +252,12 @@ For details on creating Form type extensions, read the .. _reference-dic-type_guesser: -``form.type_guesser`` ---------------------- +form.type_guesser +----------------- **Purpose**: Add your own logic for "form type guessing" -This tag allows you to add your own logic to the :ref:`form guessing ` +This tag allows you to add your own logic to the :ref:`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). @@ -453,8 +267,8 @@ metadata and Doctrine metadata (if you're using Doctrine) or Propel metadata For information on how to create your own type guesser, see :doc:`/form/type_guesser`. -``kernel.cache_clearer`` ------------------------- +kernel.cache_clearer +-------------------- **Purpose**: Register your service to be called during the cache clearing process @@ -466,8 +280,8 @@ files during the cache clearing process. In order to register your custom cache clearer, first you must create a service class:: - // src/AppBundle/Cache/MyClearer.php - namespace AppBundle\Cache; + // src/Cache/MyClearer.php + namespace App\Cache; use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface; @@ -479,7 +293,7 @@ service class:: } } -If you're using the :ref:`default services.yml configuration `, +If you're using the :ref:`default services.yaml configuration `, your service will be automatically tagged with ``kernel.cache_clearer``. But, you can also register it manually: @@ -488,7 +302,7 @@ can also register it manually: .. code-block:: yaml services: - AppBundle\Cache\MyClearer: + App\Cache\MyClearer: tags: [kernel.cache_clearer] .. code-block:: xml @@ -500,7 +314,7 @@ can also register it manually: https://symfony.com/schema/dic/services/services-1.0.xsd"> - + @@ -508,15 +322,15 @@ can also register it manually: .. code-block:: php - use AppBundle\Cache\MyClearer; + use App\Cache\MyClearer; $container ->register(MyClearer::class) ->addTag('kernel.cache_clearer') ; -``kernel.cache_warmer`` ------------------------ +kernel.cache_warmer +------------------- **Purpose**: Register your service to be called during the cache warming process @@ -532,8 +346,8 @@ is generated dynamically. To register your own cache warmer, first create a service that implements the :class:`Symfony\\Component\\HttpKernel\\CacheWarmer\\CacheWarmerInterface` interface:: - // src/Acme/MainBundle/Cache/MyCustomWarmer.php - namespace AppBundle\Cache; + // src/Cache/MyCustomWarmer.php + namespace App\Cache; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; @@ -555,7 +369,7 @@ 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). -If you're using the :ref:`default services.yml configuration `, +If you're using the :ref:`default services.yaml configuration `, your service will be automatically tagged with ``kernel.cache_warmer``. But, you can also register it manually: @@ -564,7 +378,7 @@ can also register it manually: .. code-block:: yaml services: - AppBundle\Cache\MyCustomWarmer: + App\Cache\MyCustomWarmer: tags: - { name: kernel.cache_warmer, priority: 0 } @@ -577,7 +391,7 @@ can also register it manually: https://symfony.com/schema/dic/services/services-1.0.xsd"> - + @@ -585,7 +399,7 @@ can also register it manually: .. code-block:: php - use AppBundle\Cache\MyCustomWarmer; + use App\Cache\MyCustomWarmer; $container ->register(MyCustomWarmer::class) @@ -617,8 +431,8 @@ with the following command: .. _dic-tags-kernel-event-listener: -``kernel.event_listener`` -------------------------- +kernel.event_listener +--------------------- **Purpose**: To listen to different events/hooks in Symfony @@ -637,16 +451,16 @@ see the :doc:`Symfony Events Reference `. .. _dic-tags-kernel-event-subscriber: -``kernel.event_subscriber`` ---------------------------- +kernel.event_subscriber +----------------------- **Purpose**: To subscribe to a set of different events/hooks in Symfony This is an alternative way to create an event listener, and is the recommended way (instead of using ``kernel.event_listener``). See :ref:`events-subscriber`. -``kernel.fragment_renderer`` ----------------------------- +kernel.fragment_renderer +------------------------ **Purpose**: Add a new HTTP content rendering strategy @@ -655,8 +469,8 @@ To add a new rendering strategy - in addition to the core strategies like :class:`Symfony\\Component\\HttpKernel\\Fragment\\FragmentRendererInterface`, register it as a service, then tag it with ``kernel.fragment_renderer``. -``kernel.reset`` ----------------- +kernel.reset +------------ **Purpose**: Clean up services between requests @@ -670,10 +484,21 @@ reuse the Symfony application between requests to improve performance. This tag is applied for example to the built-in :doc:`data collectors ` of the profiler to delete all their information. +.. _dic_tags-mime: + +mime.mime_type_guesser +---------------------- + +**Purpose**: Add your own logic for guessing MIME types + +This tag is used to register your own :ref:`MIME type guessers ` +in case the guessers provided by the :doc:`Mime component ` +don't fit your needs. + .. _dic_tags-monolog: -``monolog.logger`` ------------------- +monolog.logger +-------------- **Purpose**: To use a custom logging channel with Monolog @@ -686,7 +511,7 @@ channel when injecting the logger in a service. .. code-block:: yaml services: - AppBundle\Log\CustomLogger: + App\Log\CustomLogger: arguments: ['@logger'] tags: - { name: monolog.logger, channel: app } @@ -700,7 +525,7 @@ channel when injecting the logger in a service. https://symfony.com/schema/dic/services/services-1.0.xsd"> - + @@ -709,7 +534,7 @@ channel when injecting the logger in a service. .. code-block:: php - use AppBundle\Log\CustomLogger; + use App\Log\CustomLogger; use Symfony\Component\DependencyInjection\Reference; $container->register(CustomLogger::class) @@ -718,14 +543,13 @@ channel when injecting the logger in a service. .. tip:: - You can also configure custom channels in the configuration and retrieve - the corresponding logger service from the service container directly (see - :ref:`monolog-channels-config`). + You can create :doc:`custom channels ` and + even :ref:`autowire logging channels `. .. _dic_tags-monolog-processor: -``monolog.processor`` ---------------------- +monolog.processor +----------------- **Purpose**: Add a custom processor for logging @@ -854,8 +678,8 @@ You can also add a processor for a specific logging channel by using the You cannot use both the ``handler`` and ``channel`` attributes for the same tag as handlers are shared between all channels. -``routing.loader`` ------------------- +routing.loader +-------------- **Purpose**: Register a custom service that loads routes @@ -867,7 +691,7 @@ of your configuration and tag it with ``routing.loader``: .. code-block:: yaml services: - AppBundle\Routing\CustomLoader: + App\Routing\CustomLoader: tags: [routing.loader] .. code-block:: xml @@ -879,7 +703,7 @@ of your configuration and tag it with ``routing.loader``: https://symfony.com/schema/dic/services/services-1.0.xsd"> - + @@ -887,7 +711,7 @@ of your configuration and tag it with ``routing.loader``: .. code-block:: php - use AppBundle\Routing\CustomLoader; + use App\Routing\CustomLoader; $container ->register(CustomLoader::class) @@ -896,8 +720,8 @@ of your configuration and tag it with ``routing.loader``: For more information, see :doc:`/routing/custom_route_loader`. -``routing.expression_language_provider`` ----------------------------------------- +routing.expression_language_provider +------------------------------------ **Purpose**: Register a provider for expression language functions in routing @@ -906,8 +730,8 @@ This tag is used to automatically register for the routing expression component. Using these providers, you can add custom functions to the routing expression language. -``security.expression_language_provider`` ------------------------------------------ +security.expression_language_provider +------------------------------------- **Purpose**: Register a provider for expression language functions in security @@ -916,8 +740,8 @@ This tag is used to automatically register :ref:`expression function providers component. Using these providers, you can add custom functions to the security expression language. -``security.remember_me_aware`` ------------------------------- +security.remember_me_aware +-------------------------- **Purpose**: To allow remember me authentication @@ -932,8 +756,8 @@ and your custom authentication listener extends then your custom authentication listener will automatically have this tag applied and it will function automatically. -``security.voter`` ------------------- +security.voter +-------------- **Purpose**: To add a custom voter to Symfony's authorization logic @@ -945,8 +769,8 @@ For more information, read the :doc:`/security/voters` article. .. _reference-dic-tags-serializer-encoder: -``serializer.encoder`` ----------------------- +serializer.encoder +------------------ **Purpose**: Register a new encoder in the ``serializer`` service @@ -957,8 +781,8 @@ For more details, see :doc:`/serializer`. .. _reference-dic-tags-serializer-normalizer: -``serializer.normalizer`` -------------------------- +serializer.normalizer +--------------------- **Purpose**: Register a new normalizer in the Serializer service @@ -971,8 +795,8 @@ The priorities of the default normalizers can be found in the :method:`Symfony\\Bundle\\FrameworkBundle\\DependencyInjection\\FrameworkExtension::registerSerializerConfiguration` method. -``swiftmailer.default.plugin`` ------------------------------- +swiftmailer.default.plugin +-------------------------- **Purpose**: Register a custom SwiftMailer Plugin @@ -993,52 +817,10 @@ 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 :doc:`/reference/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): - -.. configuration-block:: - - .. code-block:: yaml - - services: - AppBundle\Templating\AppHelper: - tags: - - { name: templating.helper, alias: alias_name } - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - use AppBundle\Templating\AppHelper; - - $container->register(AppHelper::class) - ->addTag('templating.helper', ['alias' => 'alias_name']) - ; - .. _dic-tags-translation-loader: -``translation.loader`` ----------------------- +translation.loader +------------------ **Purpose**: To register a custom service that loads translations @@ -1052,7 +834,7 @@ Now, register your loader as a service and tag it with ``translation.loader``: .. code-block:: yaml services: - AppBundle\Translation\MyCustomLoader: + App\Translation\MyCustomLoader: tags: - { name: translation.loader, alias: bin } @@ -1065,7 +847,7 @@ Now, register your loader as a service and tag it with ``translation.loader``: https://symfony.com/schema/dic/services/services-1.0.xsd"> - + @@ -1073,7 +855,7 @@ Now, register your loader as a service and tag it with ``translation.loader``: .. code-block:: php - use AppBundle\Translation\MyCustomLoader; + use App\Translation\MyCustomLoader; $container ->register(MyCustomLoader::class) @@ -1084,8 +866,7 @@ 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``. +the ``messages`` domain, then you might have a file ``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 @@ -1098,8 +879,8 @@ the ``load()`` method on your custom loader. .. _reference-dic-tags-translation-extractor: -``translation.extractor`` -------------------------- +translation.extractor +--------------------- **Purpose**: To register a custom service that extracts messages from a file @@ -1107,7 +888,7 @@ file When executing the ``translation:update`` command, it uses extractors to extract translation messages from a file. By default, the Symfony Framework has a :class:`Symfony\\Bridge\\Twig\\Translation\\TwigExtractor` and a -:class:`Symfony\\Bundle\\FrameworkBundle\\Translation\\PhpExtractor`, which +:class:`Symfony\\Component\\Translation\\Extractor\\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 @@ -1168,13 +949,13 @@ required option: ``alias``, which defines the name of the extractor:: .. code-block:: php - use AppBundle\Translation\CustomExtractor; + use App\Translation\CustomExtractor; $container->register(CustomExtractor::class) ->addTag('translation.extractor', ['alias' => 'foo']); -``translation.dumper`` ----------------------- +translation.dumper +------------------ **Purpose**: To register a custom service that dumps messages to a file @@ -1204,7 +985,7 @@ This is the name that's used to determine which dumper should be used. .. code-block:: yaml services: - AppBundle\Translation\JsonFileDumper: + App\Translation\JsonFileDumper: tags: - { name: translation.dumper, alias: json } @@ -1217,7 +998,7 @@ This is the name that's used to determine which dumper should be used. https://symfony.com/schema/dic/services/services-1.0.xsd"> - + @@ -1225,21 +1006,21 @@ This is the name that's used to determine which dumper should be used. .. code-block:: php - use AppBundle\Translation\JsonFileDumper; + use App\Translation\JsonFileDumper; $container->register(JsonFileDumper::class) ->addTag('translation.dumper', ['alias' => 'json']); .. _reference-dic-tags-twig-extension: -``twig.extension`` ------------------- +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``. If you're using the -:ref:`default services.yml configuration `, +:ref:`default services.yaml configuration `, the service is auto-registered and auto-tagged. But, you can also register it manually: .. configuration-block:: @@ -1247,9 +1028,15 @@ the service is auto-registered and auto-tagged. But, you can also register it ma .. code-block:: yaml services: - AppBundle\Twig\AppExtension: + App\Twig\AppExtension: tags: [twig.extension] + # optionally you can define the priority of the extension (default = 0). + # Extensions with higher priorities are registered earlier. This is mostly + # useful to register late extensions that override other extensions. + App\Twig\AnotherExtension: + tags: [{ name: twig.extension, priority: -100 }] + .. code-block:: xml @@ -1259,27 +1046,36 @@ the service is auto-registered and auto-tagged. But, you can also register it ma https://symfony.com/schema/dic/services/services-1.0.xsd"> - + + + + + .. code-block:: php - use AppBundle\Twig\AppExtension; + use App\Twig\AnotherExtension; + use App\Twig\AppExtension; $container ->register(AppExtension::class) ->addTag('twig.extension') ; + $container + ->register(AnotherExtension::class) + ->addTag('twig.extension', ['priority' => -100]) + ; For information on how to create the actual Twig Extension class, see `Twig's documentation`_ on the topic or read the :doc:`/templating/twig_extension` article. -``twig.loader`` ---------------- +twig.loader +----------- **Purpose**: Register a custom service that loads Twig templates @@ -1288,7 +1084,7 @@ By default, Symfony uses only one `Twig Loader`_ - to load Twig templates from another resource, you can create a service for the new loader and tag it with ``twig.loader``. -If you use the :ref:`default services.yml configuration `, +If you use the :ref:`default services.yaml configuration `, the service will be automatically tagged thanks to autoconfiguration. But, you can also register it manually: @@ -1297,7 +1093,7 @@ also register it manually: .. code-block:: yaml services: - AppBundle\Twig\CustomLoader: + App\Twig\CustomLoader: tags: - { name: twig.loader, priority: 0 } @@ -1310,7 +1106,7 @@ also register it manually: https://symfony.com/schema/dic/services/services-1.0.xsd"> - + @@ -1318,7 +1114,7 @@ also register it manually: .. code-block:: php - use AppBundle\Twig\CustomLoader; + use App\Twig\CustomLoader; $container ->register(CustomLoader::class) @@ -1330,16 +1126,60 @@ also register it manually: The ``priority`` is optional and its value is a positive or negative integer that defaults to ``0``. Loaders with higher numbers are tried first. -``validator.constraint_validator`` ----------------------------------- +.. _reference-dic-tags-twig-runtime: + +twig.runtime +------------ + +**Purpose**: To register a custom Lazy-Loaded Twig Extension + +:ref:`Lazy-Loaded Twig Extensions ` are defined as +regular services but the need to be tagged with ``twig.runtime``. If you're using the +:ref:`default services.yaml configuration `, +the service is auto-registered and auto-tagged. But, you can also register it manually: + +.. configuration-block:: + + .. code-block:: yaml + + services: + App\Twig\AppExtension: + tags: [twig.runtime] + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + use App\Twig\AppExtension; + + $container + ->register(AppExtension::class) + ->addTag('twig.runtime') + ; + +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 :doc:`/validation/custom_constraint` article. -``validator.initializer`` -------------------------- +validator.initializer +--------------------- **Purpose**: Register a service that initializes objects before validation diff --git a/reference/events.rst b/reference/events.rst index bf411860ec8..b7eec4d8dbd 100644 --- a/reference/events.rst +++ b/reference/events.rst @@ -31,7 +31,7 @@ following information: ``kernel.request`` ~~~~~~~~~~~~~~~~~~ -**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent` +**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\RequestEvent` This event is dispatched very early in Symfony, before the controller is determined. It's useful to add information to the Request or return a Response @@ -51,16 +51,16 @@ their priorities: ``kernel.controller`` ~~~~~~~~~~~~~~~~~~~~~ -**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\FilterControllerEvent` +**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\ControllerEvent` This event is dispatched after the controller to be executed has been resolved but before executing it. It's useful to initialize things later needed by the controller, such as `param converters`_, and even to change the controller entirely:: - use Symfony\Component\HttpKernel\Event\FilterControllerEvent; + use Symfony\Component\HttpKernel\Event\ControllerEvent; - public function onKernelController(FilterControllerEvent $event) + public function onKernelController(ControllerEvent $event) { // ... @@ -82,7 +82,7 @@ their priorities: ``kernel.controller_arguments`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\FilterControllerArgumentsEvent` +**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\ControllerArgumentsEvent` This event is dispatched just before a controller is called. It's useful to configure the arguments that are going to be passed to the controller. @@ -90,7 +90,9 @@ Typically, this is used to map URL routing parameters to their corresponding named arguments; or pass the current request when the ``Request`` type-hint is found:: - public function onKernelControllerArguments(FilterControllerArgumentsEvent $event) + use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; + + public function onKernelControllerArguments(ControllerArgumentsEvent $event) { // ... @@ -112,7 +114,7 @@ their priorities: ``kernel.view`` ~~~~~~~~~~~~~~~ -**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForControllerResultEvent` +**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\ViewEvent` This event is dispatched after the controller has been executed but *only* if the controller does *not* return a :class:`Symfony\\Component\\HttpFoundation\\Response` @@ -120,9 +122,9 @@ object. It's useful to transform the returned value (e.g. a string with some HTML contents) into the ``Response`` object needed by Symfony:: use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; + use Symfony\Component\HttpKernel\Event\ViewEvent; - public function onKernelView(GetResponseForControllerResultEvent $event) + public function onKernelView(ViewEvent $event) { $value = $event->getControllerResult(); $response = new Response(); @@ -146,13 +148,15 @@ their priorities: ``kernel.response`` ~~~~~~~~~~~~~~~~~~~ -**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent` +**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\ResponseEvent` This event is dispatched after the controller or any ``kernel.view`` listener returns a ``Response`` object. It's useful to modify or replace the response before sending it back (e.g. add/modify HTTP headers, add cookies, etc.):: - public function onKernelResponse(FilterResponseEvent $event) + use Symfony\Component\HttpKernel\Event\ResponseEvent; + + public function onKernelResponse(ResponseEvent $event) { $response = $event->getResponse(); @@ -179,6 +183,8 @@ This event is dispatched after the ``kernel.response`` event. It's useful to res the global state of the application (for example, the translator listener resets the translator's locale to the one of the parent request):: + use Symfony\Component\HttpKernel\Event\FinishRequestEvent; + public function onKernelFinishRequest(FinishRequestEvent $event) { if (null === $parentRequest = $this->requestStack->getParentRequest()) { @@ -199,7 +205,7 @@ their priorities: ``kernel.terminate`` ~~~~~~~~~~~~~~~~~~~~ -**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\PostResponseEvent` +**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\TerminateEvent` This event is dispatched after the response has been sent (after the execution of the :method:`Symfony\\Component\\HttpKernel\\HttpKernel::handle` method). @@ -222,25 +228,25 @@ their priorities: ``kernel.exception`` ~~~~~~~~~~~~~~~~~~~~ -**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent` +**Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` This event is dispatched as soon as an error occurs during the handling of the HTTP request. It's useful to recover from errors or modify the exception details sent as response:: use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; + use Symfony\Component\HttpKernel\Event\ExceptionEvent; - public function onKernelException(GetResponseForExceptionEvent $event) + public function onKernelException(ExceptionEvent $event) { - $exception = $event->getException(); + $exception = $event->getThrowable(); $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); + // $event->setThrowable($exception); } .. note:: @@ -268,7 +274,7 @@ response: If you want to overwrite the status code of the exception response, which you should not without a good reason, call - ``GetResponseForExceptionEvent::allowCustomResponseCode()`` first and then + ``ExceptionEvent::allowCustomResponseCode()`` first and then set the status code on the response:: $event->allowCustomResponseCode(); diff --git a/reference/forms/twig_reference.rst b/reference/forms/twig_reference.rst deleted file mode 100644 index d3f31f3c8ec..00000000000 --- a/reference/forms/twig_reference.rst +++ /dev/null @@ -1,384 +0,0 @@ -.. index:: - single: Forms; Twig form function reference - -Twig Template Form Function and Variable Reference -================================================== - -When working with forms in a template, there are two powerful things at -your disposal: - -* :ref:`Functions ` for rendering each part - of a form; -* :ref:`Variables ` for getting *any* information - about any field. - -You'll use functions often to render your fields. Variables, on the other -hand, are less commonly-used, but infinitely powerful since you can access -a fields label, id attribute, errors and anything else about the field. - -.. _reference-form-twig-functions: - -Form Rendering Functions ------------------------- - -This reference manual covers all the possible Twig functions available for -rendering forms. There are several functions available and -each is responsible for rendering a different part of a form (e.g. labels, -errors, widgets, etc). - -.. _reference-forms-twig-form: - -``form(view, variables)`` -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Renders the HTML of a complete form. - -.. code-block:: twig - - {# 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: - -.. code-block:: twig - - {{ form_start(form) }} - {{ form_errors(form) }} - - {{ form_row(form.name) }} - {{ form_row(form.dueDate) }} - - {{ form_row(form.submit, { 'label': 'Submit me' }) }} - {{ form_end(form) }} - -.. _reference-forms-twig-start: - -``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. - -.. code-block:: twig - - {# render the start tag and change the submission method #} - {{ form_start(form, {'method': 'GET'}) }} - -.. _reference-forms-twig-end: - -``form_end(view, variables)`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Renders the end tag of a form. - -.. code-block:: twig - - {{ form_end(form) }} - -This helper also outputs ``form_rest()`` unless you set ``render_rest`` -to false: - -.. code-block:: twig - - {# don't render unrendered fields #} - {{ form_end(form, {'render_rest': false}) }} - -.. _reference-forms-twig-label: - -``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. - -.. code-block:: twig - - {{ 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 ":ref:`twig-reference-form-variables`" to learn about the ``variables`` -argument. - -.. _reference-forms-twig-errors: - -``form_errors(view)`` -~~~~~~~~~~~~~~~~~~~~~ - -Renders any errors for the given field. - -.. code-block:: twig - - {{ form_errors(form.name) }} - - {# render any "global" errors #} - {{ form_errors(form) }} - -.. _reference-forms-twig-widget: - -``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. - -.. code-block:: twig - - {# 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 ":ref:`twig-reference-form-variables`" to learn more about the ``variables`` -argument. - -.. _reference-forms-twig-row: - -``form_row(view, variables)`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Renders the "row" of a given field, which is the combination of the field's -label, errors and widget. - -.. code-block:: twig - - {# 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 ":ref:`twig-reference-form-variables`" to learn about the ``variables`` -argument. - -.. _reference-forms-twig-rest: - -``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 easier to -spot (since it'll render the field for you). - -.. code-block:: twig - - {{ form_rest(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. - -.. _form-twig-selectedchoice: - -``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). - -.. code-block:: html+twig - - .. code-block:: php - // app/config/routing.php - use Symfony\Component\Routing\RouteCollection; + // config/services.php + $container->setParameter('router.request_context.scheme', 'https'); + $container->setParameter('asset.request_context.secure', true); - $routes = new RouteCollection(); - $routes->addCollection( - // second argument is the type, which is required to enable - // the annotation reader for this resource - $loader->import("@AppBundle/Controller/", "annotation") - ); +Outside of console commands, use the ``schemes`` option to define the scheme of +each route explicitly: - return $routes; +.. configuration-block:: -For more details on loading routes, including how to prefix the paths of loaded routes, -see :doc:`/routing/external_resources`. + .. code-block:: php-annotations -.. index:: - single: Routing; Generating URLs + // src/Controller/SecurityController.php + namespace App\Controller; -Generating URLs ---------------- + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class SecurityController extends AbstractController + { + /** + * @Route("/login", name="login", schemes={"https"}) + */ + public function login() + { + // ... + } + } -The routing system should also be used to generate URLs. In reality, routing -is a bidirectional system: mapping the URL to a controller and -a route back to a URL. + .. code-block:: yaml -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:: + # config/routes.yaml + login: + path: /login + controller: App\Controller\SecurityController::login + schemes: [https] - class MainController extends Controller - { - public function showAction($slug) - { - // ... + .. code-block:: xml - // /blog/my-blog-post - $url = $this->generateUrl( - 'blog_show', - ['slug' => 'my-blog-post'] - ); - } - } + + -.. note:: + - The ``generateUrl()`` method defined in the base - :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` class is - just a shortcut for this code:: + + - $url = $this->container->get('router')->generate( - 'blog_show', - ['slug' => 'my-blog-post'] - ); + .. code-block:: php -.. index:: - single: Routing; Generating URLs in a template + // config/routes.php + use App\Controller\SecurityController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; -Generating URLs with Query Strings -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + return function (RoutingConfigurator $routes) { + $routes->add('login', '/login') + ->controller([SecurityController::class, 'login']) + ->schemes(['https']) + ; + }; -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:: +The URL generated for the ``login`` route will always use HTTPS. This means that +when using the ``path()`` Twig function to generate URLs, you may get an +absolute URL instead of a relative URL if the HTTP scheme of the original +request is different from the scheme used by the route: - $this->get('router')->generate('blog', [ - 'page' => 2, - 'category' => 'Symfony', - ]); - // /blog/2?category=Symfony +.. code-block:: twig -Generating URLs from a Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + {# if the current scheme is HTTPS, generates a relative URL: /login #} + {{ path('login') }} -To generate URLs inside Twig, see the templating article: :ref:`templating-pages`. -If you also need to generate URLs in JavaScript, see :doc:`/routing/generate_url_javascript`. + {# if the current scheme is HTTP, generates an absolute URL to change + the scheme: https://example.com/login #} + {{ path('login') }} -.. index:: - single: Routing; Absolute URLs +The scheme requirement is also enforced for incoming requests. If you try to +access the ``/login`` URL with HTTP, you will automatically be redirected to the +same URL, but with the HTTPS scheme. -Generating Absolute URLs -~~~~~~~~~~~~~~~~~~~~~~~~ +If you want to force a group of routes to use HTTPS, you can define the default +scheme when importing them. The following example forces HTTPS on all routes +defined as annotations: -By default, the router will generate relative URLs (e.g. ``/blog``). From -a controller, pass ``UrlGeneratorInterface::ABSOLUTE_URL`` to the third argument of the ``generateUrl()`` -method:: +.. configuration-block:: - use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + .. code-block:: yaml + + # config/routes/annotations.yaml + controllers: + resource: '../src/Controller/' + type: annotation + defaults: + schemes: [https] + + .. code-block:: xml + + + + + + + HTTPS + + + + .. code-block:: php - $this->generateUrl('blog_show', ['slug' => 'my-blog-post'], UrlGeneratorInterface::ABSOLUTE_URL); - // http://www.example.com/blog/my-blog-post + // config/routes/annotations.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + $routes->import('../src/Controller/', 'annotation') + ->schemes(['https']) + ; + }; .. note:: - 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 :doc:`/console/request_context` to learn how to - solve this problem. + The Security component provides + :doc:`another way to enforce HTTP or HTTPS ` + via the ``requires_channel`` setting. Troubleshooting --------------- Here are some common errors you might see while working with routing: - Controller "AppBundle\\Controller\\BlogController::showAction()" requires that you + Controller "App\\Controller\\BlogController::show()" requires that you provide a value for the "$slug" argument. This happens when your controller method has an argument (e.g. ``$slug``):: - public function showAction($slug) + public function show($slug) { // ... } -But your route path does *not* have a ``{slug}`` wildcard (e.g. it is ``/blog/show``). -Add a ``{slug}`` to your route path: ``/blog/show/{slug}`` or give the argument -a default value (i.e. ``$slug = null``). +But your route path does *not* have a ``{slug}`` parameter (e.g. it is +``/blog/show``). Add a ``{slug}`` to your route path: ``/blog/show/{slug}`` or +give the argument a default value (i.e. ``$slug = null``). Some mandatory parameters are missing ("slug") to generate a URL for route "blog_show". -This means that you're trying to generate a URL to the ``blog_show`` route but you -are *not* passing a ``slug`` value (which is required, because it has a ``{slug}``) -wildcard in the route path. To fix this, pass a ``slug`` value when generating the -route:: +This means that you're trying to generate a URL to the ``blog_show`` route but +you are *not* passing a ``slug`` value (which is required, because it has a +``{slug}`` parameter in the route path). To fix this, pass a ``slug`` value when +generating the route:: $this->generateUrl('blog_show', ['slug' => 'slug-value']); // or, in Twig - // {{ path('blog_show', {'slug': 'slug-value'}) }} - -Translating Routes ------------------- - -Symfony doesn't support defining routes with different contents depending on the -user language. In those cases, you can define multiple routes per controller, -one for each supported language; or use any of the bundles created by the -community to implement this feature, such as `JMSI18nRoutingBundle`_ and -`BeSimpleI18nRoutingBundle`_. - -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. - -Keep Going! ------------ - -Routing, check! Now, uncover the power of :doc:`controllers `. + // {{ path('blog_show', {slug: 'slug-value'}) }} Learn more about Routing ------------------------ @@ -801,5 +2217,7 @@ Learn more about Routing routing/* -.. _`JMSI18nRoutingBundle`: https://github.com/schmittjoh/JMSI18nRoutingBundle -.. _`BeSimpleI18nRoutingBundle`: https://github.com/BeSimple/BeSimpleI18nRoutingBundle +.. _`PHP regular expressions`: https://www.php.net/manual/en/book.pcre.php +.. _`PCRE Unicode properties`: http://php.net/manual/en/regexp.reference.unicode.php +.. _`full param converter documentation`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html +.. _`FOSJsRoutingBundle`: https://github.com/FriendsOfSymfony/FOSJsRoutingBundle diff --git a/routing/conditions.rst b/routing/conditions.rst deleted file mode 100644 index df0e6b47610..00000000000 --- a/routing/conditions.rst +++ /dev/null @@ -1,112 +0,0 @@ -.. index:: - single: Routing; Conditions - -How to Restrict Route Matching through Conditions -================================================= - -A route can be made to match only certain routing placeholders (via regular -expressions), HTTP methods, or host names. If you need more flexibility to -define arbitrary matching logic, use the ``condition`` routing setting: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/DefaultController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Routing\Annotation\Route; - - class DefaultController extends Controller - { - /** - * @Route( - * "/contact", - * name="contact", - * condition="context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'" - * ) - * - * expressions can also include config parameters - * condition: "request.headers.get('User-Agent') matches '%app.allowed_browsers%'" - */ - public function contact() - { - // ... - } - } - - .. code-block:: yaml - - contact: - path: /contact - defaults: { _controller: AcmeDemoBundle:Main:contact } - condition: "context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'" - - .. code-block:: xml - - - - - - AcmeDemoBundle:Main:contact - context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i' - - - - .. code-block:: php - - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('contact', new Route( - '/contact', [ - '_controller' => 'AcmeDemoBundle:Main:contact', - ], - [], - [], - '', - [], - [], - 'context.getMethod() in ["GET", "HEAD"] and request.headers.get("User-Agent") matches "/firefox/i"' - )); - - return $collection; - -The ``condition`` is an expression, and you can learn more about its syntax -here: :doc:`/components/expression_language/syntax`. With this, the route -won't match unless the HTTP method is either GET or HEAD *and* if the ``User-Agent`` -header matches ``firefox``. - -You can do any complex logic you need in the expression by leveraging two -variables that are passed into the expression: - -``context`` - An instance of :class:`Symfony\\Component\\Routing\\RequestContext`, - which holds the most fundamental information about the route being matched. -``request`` - The Symfony :class:`Symfony\\Component\\HttpFoundation\\Request` object - (see :ref:`component-http-foundation-request`). - -.. caution:: - - Conditions are *not* taken into account when generating a URL. - -.. sidebar:: Expressions are Compiled to PHP - - Behind the scenes, expressions are compiled down to raw PHP. Our example - would generate the following PHP in the cache directory:: - - if (rtrim($pathInfo, '/contact') === '' && ( - in_array($context->getMethod(), [0 => "GET", 1 => "HEAD"]) - && preg_match("/firefox/i", $request->headers->get("User-Agent")) - )) { - // ... - } - - Because of this, using the ``condition`` key causes no extra overhead - beyond the time it takes for the underlying PHP to execute. diff --git a/routing/custom_route_loader.rst b/routing/custom_route_loader.rst index 8aef02f2fda..8a66b15e38f 100644 --- a/routing/custom_route_loader.rst +++ b/routing/custom_route_loader.rst @@ -4,6 +4,90 @@ How to Create a custom Route Loader =================================== +Simple applications can define all their routes in a single configuration file - +usually ``config/routes.yaml`` (see :ref:`routing-creating-routes`). +However, in most applications it's common to import routes definitions from +different resources: PHP annotations in controller files, YAML, XML or PHP +files stored in some directory, etc. + +Built-in Route Loaders +---------------------- + +Symfony provides several route loaders for the most common needs: + +.. configuration-block:: + + .. code-block:: yaml + + # config/routes.yaml + app_file: + # loads routes from the given routing file stored in some bundle + resource: '@AcmeBundle/Resources/config/routing.yaml' + + app_annotations: + # loads routes from the PHP annotations of the controllers found in that directory + resource: '../src/Controller/' + type: annotation + + app_directory: + # loads routes from the YAML, XML or PHP files found in that directory + resource: '../legacy/routing/' + type: directory + + app_bundle: + # loads routes from the YAML, XML or PHP files found in some bundle directory + resource: '@AcmeOtherBundle/Resources/config/routing/' + type: directory + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/routes.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + // loads routes from the given routing file stored in some bundle + $routes->import('@AcmeBundle/Resources/config/routing.yaml'); + + // loads routes from the PHP annotations of the controllers found in that directory + $routes->import('../src/Controller/', 'annotation'); + + // loads routes from the YAML or XML files found in that directory + $routes->import('../legacy/routing/', 'directory'); + + // loads routes from the YAML or XML files found in some bundle directory + $routes->import('@AcmeOtherBundle/Resources/config/routing/', 'directory'); + }; + +.. note:: + + When importing resources, the key (e.g. ``app_file``) is the name of collection. + Just be sure that it's unique per file so no other lines override it. + +If your application needs are different, you can create your own custom route +loader as explained in the next section. + What is a Custom Route Loader ----------------------------- @@ -13,7 +97,7 @@ conventions or patterns. A great example for this use-case is the action methods in a controller. You still need to modify your routing configuration (e.g. -``app/config/routing.yml``) manually, even when using a custom route +``config/routes.yaml``) manually, even when using a custom route loader. .. note:: @@ -36,18 +120,41 @@ and therefore have two important methods: :method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::supports` and :method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::load`. -Take these lines from the ``routing.yml`` in the Symfony Standard Edition: +Take these lines from the ``routes.yaml``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/routes.yaml + controllers: + resource: ../src/Controller/ + type: annotation + + .. code-block:: xml + + + + + + + -.. code-block:: yaml + .. code-block:: php + + // config/routes.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - # app/config/routing.yml - app: - resource: '@AppBundle/Controller/' - type: annotation + return function (RoutingConfigurator $routes) { + $routes->import('../src/Controller', 'annotation'); + }; When the main loader parses this, it tries all registered delegate loaders and calls their :method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::supports` -method with the given resource (``@AppBundle/Controller/``) +method with the given resource (``../src/Controller/``) and type (``annotation``) as arguments. When one of the loader returns ``true``, its :method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::load` method will be called, which should return a :class:`Symfony\\Component\\Routing\\RouteCollection` @@ -56,7 +163,7 @@ containing :class:`Symfony\\Component\\Routing\\Route` objects. .. note:: Routes loaded this way will be cached by the Router the same way as - when they are defined in one of the default formats (e.g. XML, YML, + when they are defined in one of the default formats (e.g. XML, YAML, PHP file). Loading Routes with a Custom Service @@ -73,46 +180,52 @@ and configure the service and method to call: .. code-block:: yaml - # app/config/routing.yml + # config/routes.yaml admin_routes: - resource: 'admin_route_loader:loadRoutes' + resource: 'admin_route_loader::loadRoutes' type: service .. code-block:: xml - + - + .. code-block:: php - // app/config/routing.php - use Symfony\Component\Routing\RouteCollection; + // config/routes.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - $routes = new RouteCollection(); - $routes->addCollection( - $loader->import("admin_route_loader:loadRoutes", "service") - ); - - return $routes; + return function (RoutingConfigurator $routes) { + $routes->import('admin_route_loader::loadRoutes', 'service'); + }; In this example, the routes are loaded by calling the ``loadRoutes()`` method of the service whose ID is ``admin_route_loader``. Your service doesn't have to extend or implement any special class, but the called method must return a :class:`Symfony\\Component\\Routing\\RouteCollection` object. +If you're using :ref:`autoconfigure `, your class should +implement the :class:`Symfony\\Bundle\\FrameworkBundle\\Routing\\RouteLoaderInterface` +interface to be tagged automatically. If you're **not using autoconfigure**, +tag it manually with ``routing.loader``. + .. note:: The routes defined using service route loaders will be automatically cached by the framework. So whenever your service should load new routes, don't forget to clear the cache. +.. tip:: + + If your service is invokable, you don't need to precise the method to use. + Creating a custom Loader ------------------------ @@ -126,11 +239,11 @@ In most cases it is easier to extend from The sample loader below supports loading routing resources with a type of ``extra``. The type name should not clash with other loaders that might -support the same type of resource. Just make up a name specific to what +support the same type of resource. Make up any name specific to what you do. The resource name itself is not actually used in the example:: - // src/AppBundle/Routing/ExtraLoader.php - namespace AppBundle\Routing; + // src/Routing/ExtraLoader.php + namespace App\Routing; use Symfony\Component\Config\Loader\Loader; use Symfony\Component\Routing\Route; @@ -140,7 +253,7 @@ you do. The resource name itself is not actually used in the example:: { private $isLoaded = false; - public function load($resource, $type = null) + public function load($resource, string $type = null) { if (true === $this->isLoaded) { throw new \RuntimeException('Do not add the "extra" loader twice'); @@ -151,7 +264,7 @@ you do. The resource name itself is not actually used in the example:: // prepare a new route $path = '/extra/{parameter}'; $defaults = [ - '_controller' => 'AppBundle:Extra:extra', + '_controller' => 'App\Controller\ExtraController::extra', ]; $requirements = [ 'parameter' => '\d+', @@ -167,25 +280,24 @@ you do. The resource name itself is not actually used in the example:: return $routes; } - public function supports($resource, $type = null) + public function supports($resource, string $type = null) { return 'extra' === $type; } } Make sure the controller you specify really exists. In this case you -have to create an ``extraAction()`` method in the ``ExtraController`` -of the ``AppBundle``:: +have to create an ``extra()`` method in the ``ExtraController``:: - // src/AppBundle/Controller/ExtraController.php - namespace AppBundle\Controller; + // src/Controller/ExtraController.php + namespace App\Controller; - use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; - class ExtraController extends Controller + class ExtraController extends AbstractController { - public function extraAction($parameter) + public function extra($parameter) { return new Response($parameter); } @@ -197,15 +309,16 @@ Now define a service for the ``ExtraLoader``: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # ... - AppBundle\Routing\ExtraLoader: + App\Routing\ExtraLoader: tags: [routing.loader] .. code-block:: xml + - +
@@ -223,7 +336,8 @@ Now define a service for the ``ExtraLoader``: .. code-block:: php - use AppBundle\Routing\ExtraLoader; + // config/services.php + use App\Routing\ExtraLoader; $container->autowire(ExtraLoader::class) ->addTag('routing.loader') @@ -244,13 +358,14 @@ What remains to do is adding a few lines to the routing configuration: .. code-block:: yaml - # app/config/routing.yml + # config/routes.yaml app_extra: resource: . type: extra .. code-block:: xml + addCollection($loader->import('.', 'extra')); + // config/routes.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return $routes; + return function (RoutingConfigurator $routes) { + $routes->import('.', 'extra'); + }; -The important part here is the ``type`` key. Its value should be "extra" as +The important part here is the ``type`` key. Its value should be ``extra`` as 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 ".". +for the ``ExtraLoader``, so it is set to ``.`` (a single dot). .. note:: @@ -297,19 +411,19 @@ Whenever you want to load another resource - for instance a YAML routing configuration file - you can call the :method:`Symfony\\Component\\Config\\Loader\\Loader::import` method:: - // src/AppBundle/Routing/AdvancedLoader.php - namespace AppBundle\Routing; + // src/Routing/AdvancedLoader.php + namespace App\Routing; use Symfony\Component\Config\Loader\Loader; use Symfony\Component\Routing\RouteCollection; class AdvancedLoader extends Loader { - public function load($resource, $type = null) + public function load($resource, string $type = null) { $routes = new RouteCollection(); - $resource = '@AppBundle/Resources/config/import_routing.yml'; + $resource = '@ThirdPartyBundle/Resources/config/routes.yaml'; $type = 'yaml'; $importedRoutes = $this->import($resource, $type); @@ -319,7 +433,7 @@ configuration file - you can call the return $routes; } - public function supports($resource, $type = null) + public function supports($resource, string $type = null) { return 'advanced_extra' === $type; } diff --git a/routing/debug.rst b/routing/debug.rst deleted file mode 100644 index d0c4e78faea..00000000000 --- a/routing/debug.rst +++ /dev/null @@ -1,59 +0,0 @@ -.. index:: - single: Routing; Debugging - -How to Visualize And Debug 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 ``debug:router`` console command, which, -by default, lists *all* the configured routes in your application: - -.. code-block:: terminal - - $ php bin/console debug:router - - ------------------ -------- -------- ------ ---------------------------------------------- - Name Method Scheme Host Path - ------------------ -------- -------- ------ ---------------------------------------------- - homepage ANY ANY ANY / - contact GET ANY ANY /contact - contact_process POST ANY ANY /contact - article_show ANY ANY ANY /articles/{_locale}/{year}/{title}.{_format} - blog ANY ANY ANY /blog/{page} - blog_show ANY ANY ANY /blog/{slug} - ------------------ -------- -------- ------ ---------------------------------------------- - -You can also get very specific information on a single route by including -the route name as the command argument: - -.. code-block:: terminal - - $ php bin/console debug:router article_show - -Likewise, if you want to test whether a URL matches a given route, use the -``router:match`` command. This is useful to debug routing issues and find out -which route is associated with the given URL: - -.. code-block:: terminal - - $ php bin/console router:match /blog/my-latest-post - - Route "blog_show" matches - - +--------------+---------------------------------------------------------+ - | Property | Value | - +--------------+---------------------------------------------------------+ - | Route Name | blog_show | - | Path | /blog/{slug} | - | Path Regex | #^/blog/(?P[^/]++)$#sDu | - | Host | ANY | - | Host Regex | | - | Scheme | ANY | - | Method | ANY | - | Requirements | NO CUSTOM | - | Class | Symfony\Component\Routing\Route | - | Defaults | _controller: App\Controller\BlogController:show | - | Options | compiler_class: Symfony\Component\Routing\RouteCompiler | - | | utf8: true | - +--------------+---------------------------------------------------------+ diff --git a/routing/external_resources.rst b/routing/external_resources.rst deleted file mode 100644 index 4979a8744df..00000000000 --- a/routing/external_resources.rst +++ /dev/null @@ -1,190 +0,0 @@ -.. index:: - single: Routing; Importing routing resources - -How to Include External Routing Resources -========================================= - -Simple applications can define all their routes in a single configuration file - -usually ``app/config/routing.yml`` (see :ref:`routing-creating-routes`). -However, in most applications it's common to import routes definitions from -different resources: PHP annotations in controller files, YAML, XML or PHP -files stored in some directory, etc. - -This can be done by importing routing resources from the main routing file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - app_file: - # loads routes from the given routing file stored in some bundle - resource: '@AcmeBundle/Resources/config/routing.yml' - - app_annotations: - # loads routes from the PHP annotations of the controllers found in that directory - resource: '@AppBundle/Controller/' - type: annotation - - app_directory: - # loads routes from the YAML, XML or PHP files found in that directory - resource: '../legacy/routing/' - type: directory - - app_bundle: - # loads routes from the YAML, XML or PHP files found in some bundle directory - resource: '@AcmeOtherBundle/Resources/config/routing/' - type: directory - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->addCollection( - // loads routes from the given routing file stored in some bundle - $loader->import("@AcmeBundle/Resources/config/routing.yml") - - // loads routes from the PHP annotations of the controllers found in that directory - $loader->import("@AppBundle/Controller/", "annotation") - - // loads routes from the YAML or XML files found in that directory - $loader->import("../legacy/routing/", "directory") - - // loads routes from the YAML or XML files found in some bundle directory - $routes->import('@AcmeOtherBundle/Resources/config/routing/public/', 'directory'); - }; - - return $routes; - -.. note:: - - When importing resources, the key (e.g. ``app_file``) is the name of collection. - Just be sure that it's unique per file so no other lines override it. - -.. _prefixing-imported-routes: - -Prefixing the URLs of Imported Routes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can also choose to provide a "prefix" for the imported routes. For example, -to prefix all application routes with ``/site`` (e.g.``/site/blog/{slug}`` -instead of ``/blog/{slug}``): - -.. configuration-block:: - - .. code-block:: php-annotations - - use Symfony\Component\Routing\Annotation\Route; - - /** - * @Route("/site") - */ - class DefaultController - { - // ... - } - - .. code-block:: yaml - - # app/config/routing.yml - app: - resource: '@AppBundle/Controller/' - type: annotation - prefix: /site - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\RouteCollection; - - $app = $loader->import('@AppBundle/Controller/', 'annotation'); - $app->addPrefix('/site'); - - $routes = new RouteCollection(); - $routes->addCollection($app); - - return $routes; - -The path of each route being loaded from the new routing resource will now -be prefixed with the string ``/site``. - -Prefixing the Names of Imported Routes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 3.4 - - The feature to prefix route names was introduced in Symfony 3.4. - -You also have the possibility to prefix all route names defined in a controller -class with the ``name`` attribute of the ``@Route`` annotation:: - - use Symfony\Component\Routing\Annotation\Route; - - /** - * @Route(name="blog_") - */ - class BlogController extends Controller - { - /** - * @Route("/blog", name="index") - */ - public function indexAction() - { - // ... - } - - /** - * @Route("/blog/posts/{slug}", name="post") - */ - public function showAction(Post $post) - { - // ... - } - } - -In this example, the names of the routes will be ``blog_index`` and ``blog_post``. - -Adding a Host Requirement to Imported Routes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can set the host regex on imported routes. For more information, see -:ref:`component-routing-host-imported`. diff --git a/routing/extra_information.rst b/routing/extra_information.rst deleted file mode 100644 index 22a911b85c0..00000000000 --- a/routing/extra_information.rst +++ /dev/null @@ -1,102 +0,0 @@ -.. index:: - single: Routing; Extra Information - -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, and as attributes of the ``Request`` object: - -.. configuration-block:: - - .. code-block:: php-annotations - - use Symfony\Component\Routing\Annotation\Route; - - /** - * @Route(name="blog_") - */ - class BlogController - { - /** - * @Route("/blog/{page}", name="index", defaults={"page": 1, "title": "Hello world!"}) - */ - public function index($page) - { - // ... - } - } - - # config/routes.yaml - blog: - path: /blog/{page} - controller: App\Controller\BlogController::index - defaults: - page: 1 - title: "Hello world!" - - .. code-block:: yaml - - # app/config/routing.yml - blog: - path: /blog/{page} - defaults: - _controller: AppBundle:Blog:index - page: 1 - title: "Hello world!" - - .. code-block:: xml - - - - - - - AppBundle:Blog:index - 1 - Hello world! - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('blog', new Route('/blog/{page}', [ - '_controller' => 'AppBundle:Blog:index', - 'page' => 1, - 'title' => 'Hello world!', - ])); - - return $routes; - -Now, you can access this extra parameter in your controller, as an argument -to the controller method:: - - public function indexAction($page, $title) - { - // ... - } - -Alternatively, the title could be accessed through the ``Request`` object:: - - use Symfony\Component\HttpFoundation\Request; - - public function indexAction(Request $request, $page) - { - $title = $request->attributes->get('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, through -the method's argument, or from the ``Request`` object's ``attributes`` bag. diff --git a/routing/generate_url_javascript.rst b/routing/generate_url_javascript.rst deleted file mode 100644 index 0893b53de8e..00000000000 --- a/routing/generate_url_javascript.rst +++ /dev/null @@ -1,25 +0,0 @@ -How to Generate Routing URLs in JavaScript -========================================== - -If you're in a Twig template, you can use the same ``path()`` function to set -JavaScript variables. The ``escape()`` function helps escape any -non-JavaScript-safe values: - -.. code-block:: html+twig - - - -But if you *actually* need to generate routes in pure JavaScript, consider using -the `FOSJsRoutingBundle`_. It makes the following possible: - -.. code-block:: html+twig - - - -.. _`FOSJsRoutingBundle`: https://github.com/FriendsOfSymfony/FOSJsRoutingBundle diff --git a/routing/hostname_pattern.rst b/routing/hostname_pattern.rst deleted file mode 100644 index ec92f027edb..00000000000 --- a/routing/hostname_pattern.rst +++ /dev/null @@ -1,419 +0,0 @@ -.. index:: - single: Routing; Matching on Hostname - -How to Match a Route Based on the Host -====================================== - -You can also match any route with the HTTP *host* of the incoming request. - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/MainController.php - namespace AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends Controller - { - /** - * @Route("/", name="mobile_homepage", host="m.example.com") - */ - public function mobileHomepageAction() - { - // ... - } - - /** - * @Route("/", name="homepage") - */ - public function homepageAction() - { - // ... - } - } - - .. code-block:: yaml - - mobile_homepage: - path: / - host: m.example.com - defaults: { _controller: AppBundle:Main:mobileHomepage } - - homepage: - path: / - defaults: { _controller: AppBundle:Main:homepage } - - .. code-block:: xml - - - - - - AppBundle:Main:mobileHomepage - - - - AppBundle:Main:homepage - - - - .. code-block:: php - - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('mobile_homepage', new Route('/', [ - '_controller' => 'AppBundle:Main:mobileHomepage', - ], [], [], 'm.example.com')); - - $routes->add('homepage', new Route('/', [ - '_controller' => 'AppBundle:Main:homepage', - ])); - - return $routes; - -Both routes match the same path, ``/``. However, the first one will only -match 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: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/MainController.php - namespace AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends Controller - { - /** - * @Route("/", name="projects_homepage", host="{project_name}.example.com") - */ - public function projectsHomepageAction() - { - // ... - } - - /** - * @Route("/", name="homepage") - */ - public function homepageAction() - { - // ... - } - } - - .. code-block:: yaml - - projects_homepage: - path: / - host: "{project_name}.example.com" - defaults: { _controller: AppBundle:Main:projectsHomepage } - - homepage: - path: / - defaults: { _controller: AppBundle:Main:homepage } - - .. code-block:: xml - - - - - - AppBundle:Main:projectsHomepage - - - - AppBundle:Main:homepage - - - - .. code-block:: php - - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('project_homepage', new Route('/', [ - '_controller' => 'AppBundle:Main:projectsHomepage', - ], [], [], '{project_name}.example.com')); - - $routes->add('homepage', new Route('/', [ - '_controller' => 'AppBundle:Main:homepage', - ])); - - return $routes; - -Also, any requirement or default can be set for these placeholders. For -instance, if you want to match both ``m.example.com`` and -``mobile.example.com``, you can use this: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/MainController.php - namespace AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends Controller - { - /** - * @Route( - * "/", - * name="mobile_homepage", - * host="{subdomain}.example.com", - * defaults={"subdomain"="m"}, - * requirements={"subdomain"="m|mobile"} - * ) - */ - public function mobileHomepageAction() - { - // ... - } - - /** - * @Route("/", name="homepage") - */ - public function homepageAction() - { - // ... - } - } - - .. code-block:: yaml - - mobile_homepage: - path: / - host: "{subdomain}.example.com" - defaults: - _controller: AppBundle:Main:mobileHomepage - subdomain: m - requirements: - subdomain: m|mobile - - homepage: - path: / - defaults: { _controller: AppBundle:Main:homepage } - - .. code-block:: xml - - - - - - AppBundle:Main:mobileHomepage - m - m|mobile - - - - AppBundle:Main:homepage - - - - .. code-block:: php - - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('mobile_homepage', new Route('/', [ - '_controller' => 'AppBundle:Main:mobileHomepage', - 'subdomain' => 'm', - ], [ - 'subdomain' => 'm|mobile', - ], [], '{subdomain}.example.com')); - - $routes->add('homepage', new Route('/', [ - '_controller' => 'AppBundle:Main:homepage', - ])); - - return $routes; - -.. tip:: - - You can also use service parameters if you do not want to hardcode the - hostname: - - .. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/MainController.php - namespace AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends Controller - { - /** - * @Route( - * "/", - * name="mobile_homepage", - * host="m.{domain}", - * defaults={"domain"="%domain%"}, - * requirements={"domain"="%domain%"} - * ) - */ - public function mobileHomepageAction() - { - // ... - } - - /** - * @Route("/", name="homepage") - */ - public function homepageAction() - { - // ... - } - } - - .. code-block:: yaml - - mobile_homepage: - path: / - host: "m.{domain}" - defaults: - _controller: AppBundle:Main:mobileHomepage - domain: '%domain%' - requirements: - domain: '%domain%' - - homepage: - path: / - defaults: { _controller: AppBundle:Main:homepage } - - .. code-block:: xml - - - - - - AppBundle:Main:mobileHomepage - %domain% - %domain% - - - - AppBundle:Main:homepage - - - - .. code-block:: php - - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('mobile_homepage', new Route('/', [ - '_controller' => 'AppBundle:Main:mobileHomepage', - 'domain' => '%domain%', - ], [ - 'domain' => '%domain%', - ], [], 'm.{domain}')); - - $routes->add('homepage', new Route('/', [ - '_controller' => 'AppBundle:Main:homepage', - ])); - - return $routes; - -.. tip:: - - 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. - -.. _component-routing-host-imported: - -Using Host Matching of Imported Routes --------------------------------------- - -You can also set the host option on imported routes: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/MainController.php - namespace AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Routing\Annotation\Route; - - /** - * @Route(host="hello.example.com") - */ - class MainController extends Controller - { - // ... - } - - .. code-block:: yaml - - app_hello: - resource: '@AppBundle/Resources/config/routing.yml' - host: "hello.example.com" - - .. code-block:: xml - - - - - - - - .. code-block:: php - - $routes = $loader->import("@AppBundle/Resources/config/routing.php"); - $routes->setHost('hello.example.com'); - - return $routes; - -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', - '/', - [], - [], - ['HTTP_HOST' => 'm.' . $client->getContainer()->getParameter('domain')] - ); diff --git a/routing/optional_placeholders.rst b/routing/optional_placeholders.rst deleted file mode 100644 index d9eb0ea9a52..00000000000 --- a/routing/optional_placeholders.rst +++ /dev/null @@ -1,203 +0,0 @@ -.. index:: - single: Routing; Optional Placeholders - -How to Define 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: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/BlogController.php - use Symfony\Component\Routing\Annotation\Route; - - class BlogController - { - /** - * @Route("/blog") - */ - public function indexAction() - { - // ... - } - // ... - } - - .. code-block:: yaml - - # app/config/routing.yml - blog: - path: /blog - defaults: { _controller: AppBundle:Blog:index } - - .. code-block:: xml - - - - - - - AppBundle:Blog:index - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('blog', new Route('/blog', [ - '_controller' => 'AppBundle:Blog:index', - ])); - - return $routes; - -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: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/BlogController.php - - // ... - - /** - * @Route("/blog/{page}") - */ - public function indexAction($page) - { - // ... - } - - .. code-block:: yaml - - # app/config/routing.yml - blog: - path: /blog/{page} - defaults: { _controller: AppBundle:Blog:index } - - .. code-block:: xml - - - - - - - AppBundle:Blog:index - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('blog', new Route('/blog/{page}', [ - '_controller' => 'AppBundle:Blog:index', - ])); - - return $routes; - -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 ``/blog`` alone. 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: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/BlogController.php - - // ... - - /** - * @Route("/blog/{page}", defaults={"page"=1}) - */ - public function indexAction($page) - { - // ... - } - - .. code-block:: yaml - - # app/config/routing.yml - blog: - path: /blog/{page} - defaults: { _controller: AppBundle:Blog:index, page: 1 } - - .. code-block:: xml - - - - - - - AppBundle:Blog:index - 1 - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('blog', new Route('/blog/{page}', [ - '_controller' => 'AppBundle:Blog:index', - 'page' => 1, - ])); - - return $routes; - -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`` -=========== ======== ================== - -.. caution:: - - 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. ``/blog`` will not match this route). - -.. tip:: - - 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). diff --git a/routing/redirect_in_config.rst b/routing/redirect_in_config.rst deleted file mode 100644 index 72a8f883955..00000000000 --- a/routing/redirect_in_config.rst +++ /dev/null @@ -1,160 +0,0 @@ -.. index:: - single: Routing; Redirect using Framework:RedirectController - -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 -:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\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 -:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\RedirectController::urlRedirectAction` -action to redirect to this new URL: - -.. configuration-block:: - - .. code-block:: 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 - - .. code-block:: xml - - - - - - - - - - - FrameworkBundle:Redirect:urlRedirect - /app - true - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - - // load some routes - one should ultimately have the path "/app" - $appRoutes = $loader->import("@AppBundle/Controller/", "annotation"); - $appRoutes->setPrefix('/app'); - - $routes->addCollection($appRoutes); - - // redirecting the root - $routes->add('root', new Route('/', [ - '_controller' => 'FrameworkBundle:Redirect:urlRedirect', - 'path' => '/app', - 'permanent' => true, - ])); - - return $routes; - -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 -:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\RedirectController::redirectAction` -action: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - - # ... - - # redirecting the admin home - root: - path: /wp-admin - defaults: - _controller: FrameworkBundle:Redirect:redirect - route: sonata_admin_dashboard - permanent: true - - .. code-block:: xml - - - - - - - - - - FrameworkBundle:Redirect:redirect - sonata_admin_dashboard - true - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - // ... - - // redirecting the root - $routes->add('root', new Route('/wp-admin', [ - '_controller' => 'FrameworkBundle:Redirect:redirect', - 'route' => 'sonata_admin_dashboard', - 'permanent' => true, - ])); - - return $routes; - -.. caution:: - - 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. diff --git a/routing/redirect_trailing_slash.rst b/routing/redirect_trailing_slash.rst deleted file mode 100644 index 016cbd4ca4e..00000000000 --- a/routing/redirect_trailing_slash.rst +++ /dev/null @@ -1,105 +0,0 @@ -.. index:: - single: Routing; Redirect URLs with a trailing slash - -Redirect URLs with a Trailing Slash -=================================== - -The goal of this article 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 308 (*HTTP Permanent Redirect*) 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); - - // 308 (Permanent Redirect) is similar to 301 (Moved Permanently) except - // that it does not allow changing the request method (e.g. from POST to GET) - return $this->redirect($url, 308); - } - } - -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: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/RedirectingController.php - namespace AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Routing\Annotation\Route; - - class RedirectingController extends Controller - { - /** - * @Route("/{url}", name="remove_trailing_slash", - * requirements={"url" = ".*\/$"}) - */ - public function removeTrailingSlashAction(Request $request) - { - // ... - } - } - - .. code-block:: yaml - - remove_trailing_slash: - path: /{url} - defaults: { _controller: AppBundle:Redirecting:removeTrailingSlash } - requirements: - url: .*/$ - - .. code-block:: xml - - - - - AppBundle:Redirecting:removeTrailingSlash - .*/$ - - - - .. code-block:: php - - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add( - 'remove_trailing_slash', - new Route( - '/{url}', - [ - '_controller' => 'AppBundle:Redirecting:removeTrailingSlash', - ], - [ - 'url' => '.*/$', - ] - ) - ); - -.. caution:: - - 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. diff --git a/routing/requirements.rst b/routing/requirements.rst deleted file mode 100644 index 6a73b4b0b4f..00000000000 --- a/routing/requirements.rst +++ /dev/null @@ -1,300 +0,0 @@ -.. index:: - single: Routing; Requirements - -How to Define Route Requirements -================================ - -:ref:`Route requirements ` can be used to make a specific route -*only* match under specific conditions. The simplest example involves restricting -a routing ``{wildcard}`` to only match some regular expression: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/BlogController.php - namespace AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Routing\Annotation\Route; - - class BlogController extends Controller - { - /** - * @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"}) - */ - public function listAction($page) - { - // ... - } - } - - .. code-block:: yaml - - # app/config/routing.yml - blog_list: - path: /blog/{page} - defaults: { _controller: AppBundle:Blog:list } - requirements: - page: '\d+' - - .. code-block:: xml - - - - - - - AppBundle:Blog:list - \d+ - - - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('blog_list', new Route('/blog/{page}', [ - '_controller' => 'AppBundle:Blog:list', - ], [ - 'page' => '\d+', - ])); - - // ... - - return $routes; - -Thanks to the ``\d+`` requirement (i.e. a "digit" of any length), ``/blog/2`` will -match this route but ``/blog/some-string`` will *not* match. - -.. sidebar:: Earlier Routes Always Win - - Why would you ever care about requirements? If a request matches *two* routes, - then the first route always wins. By adding requirements to the first route, - you can make each route match in just the right situations. See :ref:`routing-requirements` - for an example. - -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: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/MainController.php - - // ... - class MainController extends Controller - { - /** - * @Route("/{_locale}", defaults={"_locale"="en"}, requirements={ - * "_locale"="en|fr" - * }) - */ - public function homepageAction($_locale) - { - // ... - } - } - - .. code-block:: yaml - - # app/config/routing.yml - homepage: - path: /{_locale} - defaults: { _controller: AppBundle:Main:homepage, _locale: en } - requirements: - _locale: en|fr - - .. code-block:: xml - - - - - - - AppBundle:Main:homepage - en - en|fr - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('homepage', new Route('/{_locale}', [ - '_controller' => 'AppBundle:Main:homepage', - '_locale' => 'en', - ], [ - '_locale' => 'en|fr', - ])); - - return $routes; - -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* -======= ======================== - -.. note:: - - Since Symfony 3.2, you can enable UTF-8 route matching by setting the ``utf8`` - option when declaring or importing routes. This will make e.g. a ``.`` in - requirements match any UTF-8 characters instead of just a single byte. - The option is automatically enabled whenever a route or a requirement uses any - non-ASCII UTF-8 characters or a `PCRE Unicode property`_ (``\p{xx}``, - ``\P{xx}`` or ``\X``). Note that this behavior is deprecated and a - ``LogicException`` will be thrown instead in 4.0 unless you explicitly turn - on the ``utf8`` option. - -.. tip:: - - The route requirements can also include container parameters, as explained - in :doc:`this article `. - This comes in handy when the regular expression is very complex and used - repeatedly in your application. - -.. index:: - single: Routing; Method requirement - -.. _routing-method-requirement: - -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 create an API for -your blog and you have 2 routes: One for displaying a post (on a GET or HEAD -request) and one for updating a post (on a PUT request). This can be -accomplished with the following route configuration: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/BlogApiController.php - namespace AppBundle\Controller; - - // ... - - class BlogApiController extends Controller - { - /** - * @Route("/api/posts/{id}", methods={"GET","HEAD"}) - */ - public function showAction($id) - { - // ... return a JSON response with the post - } - - /** - * @Route("/api/posts/{id}", methods={"PUT"}) - */ - public function editAction($id) - { - // ... edit a post - } - } - - .. code-block:: yaml - - # app/config/routing.yml - api_post_show: - path: /api/posts/{id} - defaults: { _controller: AppBundle:BlogApi:show } - methods: [GET, HEAD] - - api_post_edit: - path: /api/posts/{id} - defaults: { _controller: AppBundle:BlogApi:edit } - methods: [PUT] - - .. code-block:: xml - - - - - - - AppBundle:BlogApi:show - - - - AppBundle:BlogApi:edit - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('api_post_show', new Route('/api/posts/{id}', [ - '_controller' => 'AppBundle:BlogApi:show', - ], [], [], '', [], ['GET', 'HEAD'])); - - $routes->add('api_post_edit', new Route('/api/posts/{id}', [ - '_controller' => 'AppBundle:BlogApi:edit', - ], [], [], '', [], ['PUT'])); - - return $routes; - -Despite the fact that these two routes have identical paths -(``/api/posts/{id}``), the first route will match only GET or HEAD requests and -the second route will match only PUT requests. This means that you can display -and edit the post with the same URL, while using distinct controllers for the -two actions. - -.. note:: - - If no ``methods`` are specified, the route will match on *all* methods. - -.. tip:: - - If you're using HTML forms and HTTP methods *other* than ``GET`` and ``POST``, - you'll need to include a ``_method`` parameter to *fake* the HTTP method. See - :doc:`/form/action_method` for more information. - -Adding a Host Requirement -------------------------- - -You can also match on the HTTP *host* of the incoming request. For more -information, see :doc:`/routing/hostname_pattern` in the Routing -component documentation. - -Adding Dynamic Requirements with Expressions --------------------------------------------- - -For really complex requirements, you can use dynamic expressions to match *any* -information on the request. See :doc:`/routing/conditions`. - -.. _`PCRE Unicode property`: http://php.net/manual/en/regexp.reference.unicode.php diff --git a/routing/scheme.rst b/routing/scheme.rst deleted file mode 100644 index 632e1db28d2..00000000000 --- a/routing/scheme.rst +++ /dev/null @@ -1,96 +0,0 @@ -.. index:: - single: Routing; Scheme requirement - -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 with the ``schemes`` setting: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/MainController.php - namespace AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends Controller - { - /** - * @Route("/secure", name="secure", schemes={"https"}) - */ - public function secureAction() - { - // ... - } - } - - .. code-block:: yaml - - # app/config/routing.yml - secure: - path: /secure - defaults: { _controller: AppBundle:Main:secure } - schemes: [https] - - .. code-block:: xml - - - - - - - - AppBundle:Main:secure - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('secure', new Route('/secure', [ - '_controller' => 'AppBundle:Main:secure', - ], [], [], '', ['https'])); - - return $routes; - -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, even when -using the ``path()`` function: - -.. code-block:: twig - - {# If the current scheme is HTTPS #} - {{ path('secure') }} - {# generates a relative URL: /secure #} - - {# If the current scheme is HTTP #} - {{ path('secure') }} - {# generates an absolute URL: 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``. - -.. note:: - - 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 - :doc:`/security/force_https` for more details). diff --git a/routing/service_container_parameters.rst b/routing/service_container_parameters.rst deleted file mode 100644 index 2aa2809504a..00000000000 --- a/routing/service_container_parameters.rst +++ /dev/null @@ -1,210 +0,0 @@ -.. index:: - single: Routing; Service Container Parameters - -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: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/MainController.php - namespace AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends Controller - { - /** - * @Route("/{_locale}/contact", name="contact", requirements={ - * "_locale"="%app.locales%" - * }) - */ - public function contactAction() - { - // ... - } - } - - .. code-block:: yaml - - # app/config/routing.yml - contact: - path: /{_locale}/contact - defaults: { _controller: AppBundle:Main:contact } - requirements: - _locale: '%app.locales%' - - .. code-block:: xml - - - - - - - AppBundle:Main:contact - %app.locales% - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('contact', new Route('/{_locale}/contact', [ - '_controller' => 'AppBundle:Main:contact', - ], [ - '_locale' => '%app.locales%', - ])); - - return $routes; - -You can now control and set the ``app.locales`` parameter somewhere -in your container: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - parameters: - app.locales: en|es - - .. code-block:: xml - - - - - - - en|es - - - - .. code-block:: 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): - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/MainController.php - namespace AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends Controller - { - /** - * @Route("/%app.route_prefix%/contact", name="contact") - */ - public function contactAction() - { - // ... - } - } - - .. code-block:: yaml - - # app/config/routing.yml - some_route: - path: /%app.route_prefix%/contact - defaults: { _controller: AppBundle:Main:contact } - - .. code-block:: xml - - - - - - - AppBundle:Main:contact - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('some_route', new Route('/%app.route_prefix%/contact', [ - '_controller' => 'AppBundle:Main:contact', - ])); - - return $routes; - -Now make sure that the ``app.route_prefix`` parameter is set somewhere in your -container: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - parameters: - app.route_prefix: 'foo' - - .. code-block:: xml - - - - - - - foo - - - - .. code-block:: php - - // app/config/config.php - $container->setParameter('app.route_prefix', 'foo'); - -.. note:: - - 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). - -.. seealso:: - - For parameter handling within a Dependency Injection Class see - :doc:`/configuration/using_parameters_in_dic`. diff --git a/routing/slash_in_parameter.rst b/routing/slash_in_parameter.rst deleted file mode 100644 index f3571cf01f1..00000000000 --- a/routing/slash_in_parameter.rst +++ /dev/null @@ -1,97 +0,0 @@ -.. index:: - single: Routing; Allow / in route parameter - -.. _routing/slash_in_parameter: - -How to Allow a "/" Character in a Route Parameter -================================================= - -Sometimes, you need to compose URLs with parameters that can contain a slash -``/``. For example, consider the ``/share/{token}`` route. If the ``token`` -value contains a ``/`` character this route won't match. This is because Symfony -uses this character as separator between route parts. - -This article explains how you can modify a route definition so that placeholders -can contain the ``/`` character too. - -Configure the Route -------------------- - -By default, the Symfony Routing component requires that the parameters match -the following regular expression: ``[^/]+``. This means that all characters are -allowed except ``/``. - -You must explicitly allow ``/`` to be part of your placeholder by specifying -a more permissive regular expression for it: - -.. configuration-block:: - - .. code-block:: php-annotations - - use Symfony\Component\Routing\Annotation\Route; - - class DefaultController - { - /** - * @Route("/share/{token}", name="share", requirements={"token"=".+"}) - */ - public function shareAction($token) - { - // ... - } - } - - .. code-block:: yaml - - share: - path: /share/{token} - defaults: { _controller: AppBundle:Default:share } - requirements: - token: .+ - - .. code-block:: xml - - - - - - AppBundle:Default:share - .+ - - - - .. code-block:: php - - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('share', new Route('/share/{token}', [ - '_controller' => 'AppBundle:Default:share', - ], [ - 'token' => '.+', - ])); - - return $routes; - -That's it! Now, the ``{token}`` parameter can contain the ``/`` character. - -.. note:: - - If the route includes the special ``{_format}`` placeholder, you shouldn't - use the ``.+`` requirement for the parameters that allow slashes. For example, - if the pattern is ``/share/{token}.{_format}`` and ``{token}`` allows any - character, the ``/share/foo/bar.json`` URL will consider ``foo/bar.json`` - as the token and the format will be empty. This can be solved by replacing the - ``.+`` requirement by ``[^.]+`` to allow any character except dots. - -.. note:: - - If the route defines several placeholders and you apply this permissive - regular expression to all of them, the results won't be the expected. For - example, if the route definition is ``/share/{path}/{token}`` and both - ``path`` and ``token`` accept ``/``, then ``path`` will contain its contents - and the token, and ``token`` will be empty. diff --git a/security.rst b/security.rst index 9b75df1e8e5..64a4fb61cb8 100644 --- a/security.rst +++ b/security.rst @@ -10,227 +10,128 @@ Security Do you prefer video tutorials? Check out the `Symfony Security screencast series`_. Symfony's security system is incredibly powerful, but it can also be confusing -to set up. In this article 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. +to set up. Don't worry! In this article, you'll learn how to set up your app's +security system step-by-step: -Since there's a lot to talk about, this article is organized into a few big -sections: +#. :ref:`Installing security support `; -#. Initial ``security.yml`` setup (*authentication*); +#. :ref:`Create your User Class `; -#. Denying access to your app (*authorization*); +#. :ref:`Authentication & Firewalls `; -#. Fetching the current User object. +#. :ref:`Denying access to your app (authorization) `; -These are followed by a number of small (but still captivating) sections, -like :ref:`logging out ` and -:doc:`encoding user passwords `. +#. :ref:`Fetching the current User object `. -.. _security-firewalls: -.. _firewalls-authentication: - -1) Initial ``security.yml`` Setup (Authentication) --------------------------------------------------- - -The security system is configured in ``app/config/security.yml``. The default -configuration looks like this: +A few other important topics are discussed after. -.. configuration-block:: - - .. code-block:: yaml +.. _security-installation: - # app/config/security.yml - security: - providers: - in_memory: - memory: ~ - - firewalls: - dev: - pattern: ^/(_(profiler|wdt)|css|images|js)/ - security: false +1) Installation +--------------- - main: - anonymous: ~ - - .. code-block:: xml - - - - - - - - - +In applications using :ref:`Symfony Flex `, run this command to +install the security feature before using it: - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', [ - 'providers' => [ - 'in_memory' => [ - 'memory' => null, - ], - ], - 'firewalls' => [ - 'dev' => [ - 'pattern' => '^/(_(profiler|wdt)|css|images|js)/', - 'security' => false, - ], - 'main' => [ - '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. +.. code-block:: terminal -.. tip:: + $ composer require symfony/security-bundle - You can also match a request against other details of the request (e.g. host). For more - information and examples read :doc:`/security/firewall_restriction`. +.. _initial-security-yml-setup-authentication: +.. _initial-security-yaml-setup-authentication: +.. _create-user-class: -All other URLs will be handled by the ``main`` 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: +2a) Create your User Class +-------------------------- -.. image:: /_images/security/anonymous_wdt.png - :align: center +No matter *how* you will authenticate (e.g. login form or API tokens) or *where* +your user data will be stored (database, single sign-on), the next step is always the same: +create a "User" class. The easiest way is to use the `MakerBundle`_. -You'll learn later how to deny access to certain URLs or controllers. +Let's assume that you want to store your user data in the database with Doctrine: -.. tip:: +.. code-block:: terminal - Security is *highly* configurable and there's a - :doc:`Security Configuration Reference ` - that shows all of the options with some extra explanation. + $ php bin/console make:user -A) Configuring how your Users will Authenticate -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + The name of the security user class (e.g. User) [User]: + > User -The main job of a firewall is to configure *how* your users will authenticate. -Will they use a login form? HTTP basic authentication? An API token? All of the above? + Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]: + > yes -Let's start with HTTP basic authentication (the old-school prompt) and work up from there. -To activate this, add the ``http_basic`` key under your firewall: + Enter a property name that will be the unique "display" name for the user (e.g. + email, username, uuid [email] + > email -.. configuration-block:: + Does this app need to hash/check user passwords? (yes/no) [yes]: + > yes - .. code-block:: yaml + created: src/Entity/User.php + created: src/Repository/UserRepository.php + updated: src/Entity/User.php + updated: config/packages/security.yaml - # app/config/security.yml - security: - # ... +That's it! The command asks several questions so that it can generate exactly what +you need. The most important is the ``User.php`` file itself. The *only* rule about +your ``User`` class is that it *must* implement :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. +Feel free to add *any* other fields or logic you need. If your ``User`` class is +an entity (like in this example), you can use the :ref:`make:entity command ` +to add more fields. Also, make sure to make and run a migration for the new entity: - firewalls: - # ... - main: - anonymous: ~ - http_basic: ~ +.. code-block:: terminal - .. code-block:: xml + $ php bin/console make:migration + $ php bin/console doctrine:migrations:migrate - - - +.. _security-user-providers: +.. _where-do-users-come-from-user-providers: - - +2b) The "User Provider" +----------------------- - - - - - - +In addition to your ``User`` class, you also need a "User provider": a class that +helps with a few things, like reloading the User data from the session and some +optional features, like :doc:`remember me ` and +:doc:`impersonation `. - .. code-block:: php +Fortunately, the ``make:user`` command already configured one for you in your +``security.yaml`` file under the ``providers`` key. - // app/config/security.php - $container->loadFromExtension('security', [ - // ... - 'firewalls' => [ - // ... - 'main' => [ - 'anonymous' => null, - 'http_basic' => null, - ], - ], - ]); +If your ``User`` class is an entity, you don't need to do anything else. But if +your class is *not* an entity, then ``make:user`` will also have generated a +``UserProvider`` class that you need to finish. Learn more about user providers +here: :doc:`User Providers `. -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:: +.. _security-encoding-user-password: +.. _encoding-the-user-s-password: - // src/AppBundle/Controller/DefaultController.php - // ... +2c) Encoding Passwords +---------------------- - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; - - 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: +Not all applications have "users" that need passwords. *If* your users have passwords, +you can control how those passwords are encoded in ``security.yaml``. The ``make:user`` +command will pre-configure this for you: .. configuration-block:: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... - firewalls: - # ... - main: - # ... - access_control: - # require ROLE_ADMIN for /admin* - - { path: '^/admin', roles: ROLE_ADMIN } + encoders: + # use your user class name here + App\Entity\User: + # Use native password encoder + # This value auto-selects the best possible hashing algorithm + # (i.e. Sodium when available). + algorithm: auto .. code-block:: xml - + - - - + - - + .. code-block:: php - // app/config/security.php + // config/packages/security.php $container->loadFromExtension('security', [ // ... - 'firewalls' => [ - // ... - 'main' => [ - // ... - ], - ], - 'access_control' => [ - // require ROLE_ADMIN for /admin* - ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'], - ], - ]); - -.. note:: - - You'll learn more about this ``ROLE_ADMIN`` thing and denying access - later in the :ref:`security-authorization` section. - -Great! Now, if you go to ``/admin``, you'll see the HTTP basic authentication prompt: - -.. image:: /_images/security/http_basic_popup.png - :align: center - -But who can you login as? Where do users come from? - -.. _security-form-login: - -.. tip:: - - Want to use a traditional login form? Great! See :doc:`/security/form_login_setup`. - What other methods are supported? See the :doc:`Configuration Reference ` - or :doc:`build your own `. - -.. tip:: - - If your application logs users in via a third-party service such as Google, - Facebook or Twitter, check out the `HWIOAuthBundle`_ community bundle. - -.. _security-user-providers: -.. _where-do-users-come-from-user-providers: -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 -:doc:`load users from the database `, -or you can :doc:`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: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - providers: - in_memory: - memory: - users: - ryan: - password: ryanpass - roles: 'ROLE_USER' - admin: - password: kitten - roles: 'ROLE_ADMIN' - # ... - - .. code-block:: xml - - - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', [ - 'providers' => [ - 'in_memory' => [ - 'memory' => [ - 'users' => [ - 'ryan' => [ - 'password' => 'ryanpass', - 'roles' => 'ROLE_USER', - ], - 'admin' => [ - 'password' => 'kitten', - 'roles' => 'ROLE_ADMIN', - ], - ], - ], - ], + 'encoders' => [ + 'App\Entity\User' => [ + 'algorithm' => 'auto', + 'cost' => 12, + ] ], + // ... ]); -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``). - -.. seealso:: - - See :doc:`/security/multiple_user_providers` for - all the details about multiple providers setup. - -Try to login using username ``admin`` and password ``kitten``. You should -see an error! - - No encoder has been configured for account "Symfony\\Component\\Security\\Core\\User\\User" +Now that Symfony knows *how* you want to encode the passwords, you can use the +``UserPasswordEncoderInterface`` service to do this before saving your users to +the database. -To fix this, add an ``encoders`` key: +For example, by using :ref:`DoctrineFixturesBundle `, you can +create dummy database users: -.. configuration-block:: - - .. code-block:: yaml +.. code-block:: terminal - # app/config/security.yml - security: - # ... + $ php bin/console make:fixtures - encoders: - Symfony\Component\Security\Core\User\User: plaintext - # ... + The class name of the fixtures to create (e.g. AppFixtures): + > UserFixtures - .. code-block:: xml +Use this service to encode the passwords: - - - +.. code-block:: diff - - + // src/DataFixtures/UserFixtures.php - - - - + + use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; + // ... - .. code-block:: php + class UserFixtures extends Fixture + { + + private $passwordEncoder; - // app/config/security.php - $container->loadFromExtension('security', [ - // ... + + public function __construct(UserPasswordEncoderInterface $passwordEncoder) + + { + + $this->passwordEncoder = $passwordEncoder; + + } - 'encoders' => [ - 'Symfony\Component\Security\Core\User\User' => 'plaintext', - ], + public function load(ObjectManager $manager) + { + $user = new User(); // ... - ]); - -User providers load user information and put it into a ``User`` object. If -you :doc:`load users from the database ` -or :doc:`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: + + $user->setPassword($this->passwordEncoder->encodePassword( + + $user, + + 'the_new_password' + + )); -.. image:: /_images/security/symfony_loggedin_wdt.png - :align: center + // ... + } + } -Because this URL requires ``ROLE_ADMIN``, if you had logged in as ``ryan``, -this would deny you access. More on that later (:ref:`security-authorization-access-control`). +You can manually encode a password by running: -Loading Users from the Database -............................... +.. code-block:: terminal -If you'd like to load your users via the Doctrine ORM, see -:doc:`/security/entity_provider` for all the details. + $ php bin/console security:encode-password -.. _security-encoding-user-password: -.. _encoding-the-user-s-password: +.. _security-yaml-firewalls: +.. _security-firewalls: +.. _firewalls-authentication: -C) Encoding the User's Password -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +3a) Authentication & Firewalls +------------------------------ -Whether your users are stored in ``security.yml``, in a database or somewhere -else, you'll want to encode their passwords. The most suitable algorithm to use -is ``bcrypt``: +The security system is configured in ``config/packages/security.yaml``. The *most* +important section is ``firewalls``: .. configuration-block:: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: - # ... - - encoders: - Symfony\Component\Security\Core\User\User: - algorithm: bcrypt - cost: 12 + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + anonymous: lazy .. code-block:: xml - + - - - + - + + + .. code-block:: php - // app/config/security.php + // config/packages/security.php $container->loadFromExtension('security', [ - // ... - - 'encoders' => [ - 'Symfony\Component\Security\Core\User\User' => [ - 'algorithm' => 'bcrypt', - 'cost' => 12, - ] + 'firewalls' => [ + 'dev' => [ + 'pattern' => '^/(_(profiler|wdt)|css|images|js)/', + 'security' => false, + ), + 'main' => [ + 'anonymous' => null, + ], ], - // ... ]); -Your users' passwords now need to be encoded with this exact algorithm. -For hardcoded users, you can use the built-in command: - -.. code-block:: terminal +A "firewall" is your authentication system: the configuration below it defines +*how* your users will be able to authenticate (e.g. login form, API token, etc). - $ php bin/console security:encode-password +Only one firewall is active on each request: Symfony uses the ``pattern`` key +to find the first match (you can also :doc:`match by host or other things `). +The ``dev`` firewall is really a fake firewall: it just makes sure that you don't +accidentally block Symfony's dev tools - which live under URLs like ``/_profiler`` +and ``/_wdt``. -It will give you something like this: +All *real* URLs are handled by the ``main`` firewall (no ``pattern`` key means +it matches *all* URLs). But this does *not* mean that every URL requires authentication. +Nope, thanks to the ``anonymous`` key, this firewall *is* accessible anonymously. -.. configuration-block:: +In fact, if you go to the homepage right now, you *will* have access and you'll see +that you're "authenticated" as ``anon.``. Don't be fooled by the "Yes" next to +Authenticated. The firewall verified that it does not know your identity, and so, +you are anonymous: - .. code-block:: yaml +.. image:: /_images/security/anonymous_wdt.png + :align: center - # app/config/security.yml - security: - # ... +You'll learn later how to deny access to certain URLs or controllers. - providers: - in_memory: - memory: - users: - ryan: - password: $2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli - roles: 'ROLE_USER' - admin: - password: $2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G - roles: 'ROLE_ADMIN' +.. note:: - .. code-block:: xml + If you do not see the toolbar, install the :doc:`profiler ` with: - - - + .. code-block:: terminal - - + $ composer require --dev symfony/profiler-pack - - - - - - - - +Now that we understand our firewall, the next step is to create a way for your +users to authenticate! - .. code-block:: php +.. _security-form-login: - // app/config/security.php - $container->loadFromExtension('security', [ - // ... +3b) Authenticating your Users +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 'providers' => [ - 'in_memory' => [ - 'memory' => [ - 'users' => [ - 'ryan' => [ - 'password' => '$2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli', - 'roles' => 'ROLE_USER', - ], - 'admin' => [ - 'password' => '$2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G', - 'roles' => 'ROLE_ADMIN', - ], - ], - ], - ], - ], - // ... - ]); +Authentication in Symfony can feel a bit "magic" at first. That's because, instead +of building a route & controller to handle login, you'll activate an +*authentication provider*: some code that runs automatically *before* your controller +is called. -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 -:doc:`/security/password_encoding` for details. +Symfony has several :doc:`built-in authentication providers `. +If your use-case matches one of these *exactly*, great! But, in most cases - including +a login form - *we recommend building a Guard Authenticator*: a class that allows +you to control *every* part of the authentication process (see the next section). .. tip:: - Supported algorithms for this method depend on your PHP version, but - include the algorithms returned by the PHP function :phpfunction:`hash_algos` - as well as a few others (e.g. bcrypt and argon2i). See the ``encoders`` key - in the :doc:`Security Reference Section ` - for examples. - - It's also possible to use different hashing algorithms on a user-by-user - basis. See :doc:`/security/named_encoders` for more details. - -D) Configuration Done! -~~~~~~~~~~~~~~~~~~~~~~ - -Congratulations! You now have a working authentication system that uses HTTP -basic authentication and loads users right from the ``security.yml`` file. + If your application logs users in via a third-party service such as Google, + Facebook or Twitter (social login), check out the `HWIOAuthBundle`_ community + bundle. -Your next steps depend on your setup: +Guard Authenticators +.................... -* Configure a different way for your users to login, like a :ref:`login form ` - or :doc:`something completely custom `; +A Guard authenticator is a class that gives you *complete* control over your +authentication process. There are *many* different ways to build an authenticator, +so here are a few common use-cases: -* Load users from a different source, like the :doc:`database ` - or :doc:`some other source `; +* :doc:`/security/form_login_setup` +* :doc:`/security/guard_authentication` -* Learn how to deny access, load the User object and deal with roles in the - :ref:`Authorization ` section. +For the most detailed description of authenticators and how they work, see +:doc:`/security/guard_authentication`. .. _`security-authorization`: +.. _denying-access-roles-and-other-authorization: -2) Denying Access, Roles and other Authorization +4) 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, ...). +Users can now log in to your app using your login form. 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: @@ -639,43 +363,42 @@ The process of authorization has two different sides: "attribute" (most commonly a role like ``ROLE_ADMIN``) in order to be accessed. -.. tip:: - - In addition to roles (e.g. ``ROLE_ADMIN``), you can protect a resource - using other attributes/strings (e.g. ``EDIT``) and use voters 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 :ref:`security-secure-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. +When a user logs in, Symfony calls the ``getRoles()`` method on your ``User`` +object to determine which roles this user has. In the ``User`` class that we +generated earlier, the roles are an array that's stored in the database, and +every user is *always* given at least one role: ``ROLE_USER``:: -.. caution:: + // src/Entity/User.php + // ... - 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 - :ref:`below ` will not work). + /** + * @ORM\Column(type="json") + */ + private $roles = []; -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. + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; -.. tip:: + return array_unique($roles); + } + +This is a nice default, but you can do *whatever* you want to determine which roles +a user should have. Here are a few guidelines: + +* Every role **must start with** ``ROLE_`` (otherwise, things won't work as expected) - 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``. +* Other than the above rule, a role is just a string and you can invent what you + need (e.g. ``ROLE_PRODUCT_ADMIN``). -You can also specify a :ref:`role hierarchy ` where -some roles automatically mean that you also have other roles. +You'll use these roles next to grant access to specific sections of your site. +You can also use a :ref:`role hierarchy ` where having +some roles automatically give you other roles. .. _security-role-authorization: @@ -684,25 +407,25 @@ Add Code to Deny Access There are **two** ways to deny access to something: -#. :ref:`access_control in security.yml ` +#. :ref:`access_control in security.yaml ` allows you to protect URL patterns (e.g. ``/admin/*``). Simpler, but less flexible; -#. :ref:`in your code via the security.authorization_checker service `. +#. :ref:`in your controller (or other code) `. .. _security-authorization-access-control: Securing URL patterns (access_control) ...................................... -The most basic way to secure part of your application is to secure an entire -URL pattern. You saw this earlier, where anything matching the regular expression -``^/admin`` requires the ``ROLE_ADMIN`` role: +The most basic way to secure part of your app is to secure an entire URL pattern +in ``security.yaml``. For example, to require ``ROLE_ADMIN`` for all URLs that +start with ``/admin``, you can: .. configuration-block:: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -724,7 +447,7 @@ URL pattern. You saw this earlier, where anything matching the regular expressio .. code-block:: xml - + loadFromExtension('security', [ // ... @@ -779,30 +502,28 @@ URL pattern. You saw this earlier, where anything matching the regular expressio ], ]); -This is great for securing entire sections, but you'll also probably want -to :ref:`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. +**BUT**, only **one** will be matched per request: Symfony starts at the top of +the list and stops when it finds the first match: .. configuration-block:: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... access_control: + # matches /admin/users/* - { path: '^/admin/users', roles: ROLE_SUPER_ADMIN } + + # matches /admin/* except for anything matching the above rule - { path: '^/admin', roles: ROLE_ADMIN } .. code-block:: xml - + loadFromExtension('security', [ // ... @@ -834,17 +555,9 @@ Prepending the path with ``^`` means that only URLs *beginning* with the pattern are matched. For example, a path of ``/admin`` (without the ``^``) would match ``/admin/foo`` but would also match URLs like ``/foo/admin``. -.. _security-access-control-explanation: - -.. sidebar:: Understanding how ``access_control`` Works - - The ``access_control`` section is very powerful, but it can also be dangerous - (because it involves security) if you don't understand *how* it works. - In addition to the URL, the ``access_control`` can match on IP address, - host name and HTTP methods. It can also be used to redirect a user to - the ``https`` version of a URL pattern. - - To learn about all of this, see :doc:`/security/access_control`. +Each ``access_control`` can also match on IP address, hostname and HTTP methods. +It can also be used to redirect a user to the ``https`` version of a URL pattern. +See :doc:`/security/access_control`. .. _security-securing-controller: @@ -853,45 +566,57 @@ Securing Controllers and other Code You can deny access from inside a controller:: + // src/Controller/AdminController.php // ... - public function helloAction($name) + public function adminDashboard() { - // The second parameter is used to specify on what object the role is tested. - $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'Unable to access this page!'); - - // Old way: - // if (false === $this->get('security.authorization_checker')->isGranted('ROLE_ADMIN')) { - // throw $this->createAccessDeniedException('Unable to access this page!'); - // } + $this->denyAccessUnlessGranted('ROLE_ADMIN'); - // ... + // or add an optional message - seen by developers + $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'User tried to access a page without having ROLE_ADMIN'); } -In both cases, a special +That's it! If access is not granted, a special :class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException` -is thrown, which ultimately triggers a 403 HTTP response inside Symfony. +is thrown and no more code in your controller is executed. Then, one of two things +will happen: + +1) If the user isn't logged in yet, they will be asked to log in (e.g. redirected + to the login page). -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, but do *not* have the -``ROLE_ADMIN`` role, they'll be shown the 403 access denied page (which you can -:ref:`customize `). If they are logged in -and have the correct roles, the code will be executed. +2) If the user *is* logged in, but does *not* have the ``ROLE_ADMIN`` role, they'll + be shown the 403 access denied page (which you can + :ref:`customize `). .. _security-securing-controller-annotations: Thanks to the SensioFrameworkExtraBundle, you can also secure your controller -using annotations:: +using annotations: + +.. code-block:: diff + // src/Controller/AdminController.php // ... - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; - /** - * @Security("has_role('ROLE_ADMIN')") - */ - public function helloAction($name) + + use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; + + + /** + + * Require ROLE_ADMIN for *every* controller method in this class. + + * + + * @IsGranted("ROLE_ADMIN") + + */ + class AdminController extends AbstractController { - // ... + + /** + + * Require ROLE_ADMIN for only this controller method. + + * + + * @IsGranted("ROLE_ADMIN") + + */ + public function adminDashboard() + { + // ... + } } For more information, see the `FrameworkExtraBundle documentation`_. @@ -901,8 +626,8 @@ For more information, see the `FrameworkExtraBundle documentation`_. Access Control in Templates ........................... -If you want to check if the current user has a role inside a template, use -the built-in ``is_granted()`` helper function: +If you want to check if the current user has a certain role, you can use +the built-in ``is_granted()`` helper function in any Twig template: .. code-block:: html+twig @@ -913,37 +638,31 @@ the built-in ``is_granted()`` helper function: Securing other Services ....................... -Anything in Symfony can be protected by doing something similar to the code -used to secure a controller. 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 :doc:`/security/securing_services`. +See :doc:`/security/securing_services`. 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 use -``IS_AUTHENTICATED_FULLY``:: +If you *only* want to check if a user is logged in (you don't care about roles), +you have two options. First, if you've given *every* user ``ROLE_USER``, you can +just check for that role. Otherwise, you can use a special "attribute" in place +of a role:: // ... - public function helloAction($name) + public function adminDashboard() { $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); // ... } -.. tip:: - - You can also use this in ``access_control``. +You can use ``IS_AUTHENTICATED_FULLY`` anywhere roles are used: like +``access_control`` or in Twig. ``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: +user that has logged in will have this. Actually, there are 3 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 @@ -958,149 +677,84 @@ special attributes like this: this - this is useful when *whitelisting* URLs to guarantee access - some details are in :doc:`/security/access_control`. -.. _security-template-expression: - -You can also use expressions inside your templates: - -.. configuration-block:: - - .. code-block:: html+twig - - {% if is_granted(expression( - '"ROLE_ADMIN" in roles or (not is_anonymous() and user.isSuperAdmin())' - )) %} - Delete - {% endif %} - - .. code-block:: html+php - - isGranted(new Expression( - '"ROLE_ADMIN" in roles or (not is_anonymous() and user.isSuperAdmin())' - ))): ?> - Delete - - -For more details on expressions and security, see :doc:`/security/expressions`. - .. _security-secure-objects: Access Control Lists (ACLs): Securing individual Database Objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 3.4 - - ACL support was deprecated in Symfony 3.4 and will be removed in 4.0. Install - the `Symfony ACL bundle`_ if you want to keep using ACL. - 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. +other users. Also, as the admin user, you want to be able to edit *all* comments. -To accomplish this you have 2 options: +:doc:`Voters ` allow you to write *whatever* business logic you +need (e.g. the user can edit this post because they are the creator) to determine +access. That's why voters are officially recommended by Symfony to create ACL-like +security systems. -* :doc:`Voters ` allow you to write own 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. +If you still prefer to use traditional ACLs, refer to the `Symfony ACL bundle`_. -* :doc:`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. +.. _retrieving-the-user-object: -In both cases, you'll still deny access using methods similar to what was -shown above. - -3) Retrieving the User Object ------------------------------ +5a) Fetching the User Object +---------------------------- After authentication, the ``User`` object of the current user can be accessed -via the ``getUser()`` shortcut (which uses the ``security.token_storage`` -service). From inside a controller, this will look like:: +via the ``getUser()`` shortcut:: - public function indexAction() + public function index() { + // usually you'll want to make sure the user is authenticated first $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + // returns your User object, or null if the user is not authenticated + // use inline documentation to tell your editor your exact User class + /** @var \App\Entity\User $user */ $user = $this->getUser(); - } - -.. tip:: - - The user will be an object and the class of that object will depend on - your :ref:`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() - { - // ... + // Call whatever methods you've added to your User class + // For example, if you added a getFirstName() method, you can use that. 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 -:ref:`access_control `) to do this:: - - // yay! Use this to see if the user is logged in - if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) { - throw $this->createAccessDeniedException(); - } - // equivalent shortcut: - // $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); - - // boo :(. Never check for the User object to see if they're logged in - if ($this->getUser()) { +5b) Fetching the User from a Service +------------------------------------ - } +If you need to get the logged in user from a service, use the +:class:`Symfony\\Component\\Security\\Core\\Security` service:: -.. note:: + // src/Service/ExampleService.php + // ... - An alternative way to get the current user in a controller is to type-hint - the controller argument with - :class:`Symfony\\Component\\Security\\Core\\Security`:: + use Symfony\Component\Security\Core\Security; - use Symfony\Component\Security\Core\Security; + class ExampleService + { + private $security; - public function indexAction(Security $security) + public function __construct(Security $security) { - $user = $security->getUser(); + // Avoid calling getUser() in the constructor: auth may not + // be complete yet. Instead, store the entire Security object. + $this->security = $security; } - .. versionadded:: 3.4 - - The ``Security`` utility class was introduced in Symfony 3.4. - - This is only recommended for experienced developers who don't extend from the - :ref:`Symfony base controller ` and - don't use the :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\ControllerTrait` - either. Otherwise, it's recommended to keep using the ``getUser()`` shortcut. + public function someMethod() + { + // returns User object or null if not authenticated + $user = $this->security->getUser(); + } + } -Retrieving the User in a Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Fetch the User in a Template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In a Twig Template this object can be accessed via the :ref:`app.user ` -key: +In a Twig Template the user object is available via the ``app.user`` variable +thanks to the :ref:`Twig global app variable `: .. code-block:: html+twig {% if is_granted('IS_AUTHENTICATED_FULLY') %} -

Username: {{ app.user.username }}

+

Email: {{ app.user.email }}

{% endif %} .. _security-logging-out: @@ -1108,36 +762,28 @@ key: Logging Out ----------- -.. caution:: - - Notice that when using HTTP basic authenticated firewalls, there is no - real way to log out : the only way to *log out* is to have the browser - stop sending your name and password on every request. Clearing your - browser cache or restarting your browser usually helps. Some web developer - tools might be helpful here too. - -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: +To enable logging out, activate the ``logout`` config parameter under your firewall: .. configuration-block:: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... firewalls: - secured_area: + main: # ... logout: - path: /logout - target: / + path: app_logout + + # where to redirect after logout + # target: app_any_route .. code-block:: xml - + - + .. code-block:: php - // app/config/security.php + // config/packages/security.php $container->loadFromExtension('security', [ // ... 'firewalls' => [ 'secured_area' => [ // ... - 'logout' => ['path' => '/logout', 'target' => '/'], + 'logout' => ['path' => 'app_logout'], ], ], ]); @@ -1173,62 +819,78 @@ Next, you'll need to create a route for this URL (but not a controller): .. configuration-block:: + .. code-block:: php-annotations + + // src/Controller/SecurityController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class SecurityController extends AbstractController + { + /** + * @Route("/logout", name="app_logout", methods={"GET"}) + */ + public function logout() + { + // controller can be blank: it will never be executed! + throw new \Exception('Don\'t forget to activate logout in security.yaml'); + } + } + .. code-block:: yaml - # app/config/routing.yml - logout: + # config/routes.yaml + app_logout: path: /logout + methods: GET .. code-block:: xml - + - + .. code-block:: php - // app/config/routing.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; - - $routes = new RouteCollection(); - $routes->add('logout', new Route('/logout')); + // config/routes.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return $routes; + return function (RoutingConfigurator $routes) { + $routes->add('logout', '/logout') + ->methods(['GET']) + ; + }; -And that's it! By sending a user to ``/logout`` (or whatever you configure -the ``path`` to be), Symfony will unauthenticate 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``). +And that's it! By sending a user to the ``app_logout`` route (i.e. to ``/logout``) +Symfony will un-authenticate the current user and redirect them. .. tip:: - 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 + Need more control of what happens after logout? Add a ``success_handler`` key + under ``logout`` and point it to a service id of a class that implements :class:`Symfony\\Component\\Security\\Http\\Logout\\LogoutSuccessHandlerInterface`. - See :doc:`Security Configuration Reference `. .. _security-role-hierarchy: Hierarchical Roles ------------------ -Instead of associating many roles to users, you can define role inheritance +Instead of giving many roles to each user, you can define role inheritance rules by creating a role hierarchy: .. configuration-block:: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -1238,7 +900,7 @@ rules by creating a role hierarchy: .. code-block:: xml - + loadFromExtension('security', [ // ... @@ -1269,31 +931,62 @@ rules by creating a role hierarchy: ], ]); -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``). - -.. note:: +Users with the ``ROLE_ADMIN`` role will also have the +``ROLE_USER`` role. And users with ``ROLE_SUPER_ADMIN``, will automatically have +``ROLE_ADMIN``, ``ROLE_ALLOWED_TO_SWITCH`` and ``ROLE_USER`` (inherited from ``ROLE_ADMIN``). - The value of the ``role_hierarchy`` option is defined statically, so you - can't for example store the role hierarchy in a database. If you need that, - create a custom :doc:`security voter ` that looks for the - user roles in the database. +For role hierarchy to work, do not try to call ``$user->getRoles()`` manually. +For example, in a controller extending from the :ref:`base controller `:: -Final Words ------------ + // BAD - $user->getRoles() will not know about the role hierarchy + $hasAccess = in_array('ROLE_ADMIN', $user->getRoles()); -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!). + // GOOD - use of the normal security methods + $hasAccess = $this->isGranted('ROLE_ADMIN'); + $this->denyAccessUnlessGranted('ROLE_ADMIN'); -Fortunately, there are a lot of articles aimed at describing many of these -situations. Also, see the :doc:`Security Reference Section `. -Many of the options don't have specific details, but seeing the full possible -configuration tree may be useful. +.. note:: -Good luck! + The ``role_hierarchy`` values are static - you can't, for example, store the + role hierarchy in a database. If you need that, create a custom + :doc:`security voter ` that looks for the user roles + in the database. + +Frequently Asked Questions +-------------------------- + +**Can I have Multiple Firewalls?** + Yes! But it's usually not necessary. Each firewall is like a separate security + system. And so, unless you have *very* different authentication needs, one + firewall usually works well. With :doc:`Guard authentication `, + you can create various, diverse ways of allowing authentication (e.g. form login, + API key authentication and LDAP) all under the same firewall. + +**Can I Share Authentication Between Firewalls?** + Yes, but only with some configuration. 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 + :ref:`reference-security-firewall-context` for different firewalls. But usually + for most applications, having one main firewall is enough. + +**Security doesn't seem to work on my Error Pages** + 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 :doc:`/controller/error_pages` + for more details. + +**My Authentication Doesn't Seem to Work: No Errors, but I'm Never Logged In** + Sometimes authentication may be successful, but after redirecting, you're + logged out immediately due to a problem loading the ``User`` from the session. + To see if this is an issue, check your log file (``var/log/dev.log``) for + the log message: + +**Cannot refresh token because user has changed** + If you see this, there are two possible causes. First, there may be a problem + loading your User from the session. See :ref:`user_session_refresh`. Second, + if certain user information was changed in the database since the last page + refresh, Symfony will purposely log out the user for security reasons. Learn More ---------- @@ -1305,24 +998,20 @@ Authentication (Identifying/Logging in the User) :maxdepth: 1 security/form_login_setup - security/ldap - security/entity_provider + security/json_login_setup security/guard_authentication + security/password_migration + security/auth_providers + security/user_provider + security/ldap security/remember_me security/impersonating_user - security/form_login - security/custom_provider - security/custom_password_authenticator - security/api_key_authentication - security/custom_authentication_provider - security/pre_authenticated - security/csrf_in_login_form + security/user_checkers security/named_encoders - security/multiple_user_providers security/multiple_guard_authenticators security/firewall_restriction - security/host_restriction - security/user_checkers + security/csrf + security/custom_authentication_provider Authorization (Denying Access) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1331,23 +1020,14 @@ Authorization (Denying Access) :maxdepth: 1 security/voters - security/acl - security/acl_advanced - security/force_https security/securing_services security/access_control security/access_denied_handler - -Other Security Related Topics -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. toctree:: - :maxdepth: 1 - - security/password_encoding - security/security_checker + security/acl + security/force_https .. _`FrameworkExtraBundle documentation`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html .. _`HWIOAuthBundle`: https://github.com/hwi/HWIOAuthBundle .. _`Symfony ACL bundle`: https://github.com/symfony/acl-bundle -.. _`Symfony Security screencast series`: https://symfonycasts.com/screencast/symfony3-security +.. _`Symfony Security screencast series`: https://symfonycasts.com/screencast/symfony-security +.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html diff --git a/security/_supportsToken.rst.inc b/security/_supportsToken.rst.inc index aede5833cfb..e4403123a01 100644 --- a/security/_supportsToken.rst.inc +++ b/security/_supportsToken.rst.inc @@ -5,6 +5,6 @@ to be used for the same firewall (that way, you can for instance first try to authenticate the user via a certificate or an API key and fall back to a form login). -Mostly, you just need to make sure that this method returns ``true`` for a +Essentially, you need to make sure that this method returns ``true`` for a token that has been created by ``createToken()``. Your logic should probably look exactly like this example. diff --git a/security/access_control.rst b/security/access_control.rst index 1156276f928..6fe3c1299cd 100644 --- a/security/access_control.rst +++ b/security/access_control.rst @@ -1,3 +1,5 @@ +.. _security-access-control-explanation: + How Does the Security access_control Work? ========================================== @@ -24,6 +26,7 @@ options are used for matching: * ``path``: a regular expression (without delimiters) * ``ip`` or ``ips``: netmasks are also supported +* ``port``: an integer * ``host``: a regular expression * ``methods``: one or many methods @@ -33,19 +36,18 @@ Take the following ``access_control`` entries as an example: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... access_control: - { path: '^/admin', roles: ROLE_USER_IP, ip: 127.0.0.1 } + - { path: '^/admin', roles: ROLE_USER_PORT, ip: 127.0.0.1, port: 8080 } - { path: '^/admin', roles: ROLE_USER_HOST, host: symfony\.com$ } - { path: '^/admin', roles: ROLE_USER_METHOD, methods: [POST, PUT] } - # when defining multiple roles, users must have at least one of them (it's like an OR condition) - - { path: '^/admin', roles: [ROLE_MANAGER, ROLE_ADMIN] } .. code-block:: xml - + + - - .. code-block:: php - // app/config/security.php + // config/packages/security.php $container->loadFromExtension('security', [ // ... 'access_control' => [ @@ -76,55 +77,53 @@ Take the following ``access_control`` entries as an example: ], [ 'path' => '^/admin', - 'roles' => 'ROLE_USER_HOST', - 'host' => 'symfony\.com$', + 'roles' => 'ROLE_USER_PORT', + 'ip' => '127.0.0.1', + 'port' => '8080', ], [ 'path' => '^/admin', - 'roles' => 'ROLE_USER_METHOD', - 'methods' => 'POST, PUT', + 'rolse' => 'ROLE_USER_HOST', + 'host' => 'symfony\.com$', ], [ 'path' => '^/admin', - // when defining multiple roles, users must have at least one of them (it's like an OR condition) - 'roles' => ['ROLE_MANAGER', 'ROLE_ADMIN'], - ], + 'roles' => 'ROLE_USER_METHOD', + 'methods' => 'POST, PUT', + ] ], ]); 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 -``ips``, ``host`` or ``methods`` are not specified for an entry, that -``access_control`` will match any ``ips``, ``host`` or ``methods``: - -+-----------------+-------------+-----------------+------------+--------------------------------+-------------------------------------------------------------+ -| 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_MANAGER``) | The ``ip``, ``host`` and ``method`` prevent the first | -| | | | | | three entries from matching. But since the URI matches the | -| | | | | | ``path`` pattern, then the ``ROLE_MANAGER`` (or the | -| |    | | | | ``ROLE_ADMIN``) 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. | -+-----------------+-------------+-----------------+------------+--------------------------------+-------------------------------------------------------------+ +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``, ``port``, ``host`` or ``method`` are not specified for an entry, that +``access_control`` will match any ``ip``, ``port``, ``host`` or ``method``: + ++-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ +| URI | IP | PORT | HOST | METHOD | ``access_control`` | Why? | ++=================+=============+=============+=============+============+================================+=============================================================+ +| ``/admin/user`` | 127.0.0.1 | 80 | example.com | GET | rule #1 (``ROLE_USER_IP``) | The URI matches ``path`` and the IP matches ``ip``. | ++-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ +| ``/admin/user`` | 127.0.0.1 | 80 | 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`` | 127.0.0.1 | 8080 | symfony.com | GET | rule #2 (``ROLE_USER_PORT``) | The ``path``, ``ip`` and ``port`` match. | ++-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ +| ``/admin/user`` | 168.0.0.1 | 80 | symfony.com | GET | rule #3 (``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 | 80 | symfony.com | POST | rule #3 (``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 | 80 | example.com | POST | rule #4 (``ROLE_USER_METHOD``) | The ``ip`` and ``host`` don't match the first two entries, | +| | | | | | | but the third - ``ROLE_USER_METHOD`` - matches and is used. | ++-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ +| ``/foo`` | 127.0.0.1 | 80 | 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. | ++-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ .. caution:: @@ -197,7 +196,7 @@ pattern so that it is only accessible by requests from the local server itself: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... access_control: @@ -208,7 +207,7 @@ pattern so that it is only accessible by requests from the local server itself: .. code-block:: xml - + loadFromExtension('security', [ // ... 'access_control' => [ @@ -282,7 +281,7 @@ key: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... access_control: @@ -295,7 +294,7 @@ key: .. code-block:: xml - + + loadFromExtension('security', [ + // ... 'access_control' => [ [ 'path' => '^/_internal/secure', @@ -323,6 +326,7 @@ key: 'allow_if' => '"127.0.0.1" == request.getClientIp() or request.header.has('X-Secure-Access')', ], ], + ]); In this case, when the user tries to access any URL starting with ``/_internal/secure``, they will only be granted access if the IP address is @@ -342,10 +346,65 @@ and functions including ``request``, which is the Symfony For a list of the other functions and variables, see :ref:`functions and variables `. +.. tip:: + + The ``allow_if`` expressions can also contain custom functions registered + with :ref:`expression providers `. + +Restrict to a port +------------------ + +Add the ``port`` option to any ``access_control`` entries to require users to +access those URLs via a specific port. This could be useful for example for +``localhost:8080``. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + access_control: + - { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, port: 8080 } + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + // ... + 'access_control' => [ + [ + 'path' => '^/cart/checkout', + 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', + 'port' => '8080', + ], + ], + ]); + Forcing a Channel (http, https) ------------------------------- -You can also require a user to access a URL via SSL; just use the +You can also require a user to access a URL via SSL; 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``: @@ -354,7 +413,7 @@ the user will be redirected to ``https``: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... access_control: @@ -362,7 +421,7 @@ the user will be redirected to ``https``: .. code-block:: xml - + - + + + + .. code-block:: php - // app/config/security.php + // config/packages/security.php $container->loadFromExtension('security', [ + // ... 'access_control' => [ [ 'path' => '^/cart/checkout', diff --git a/security/access_denied_handler.rst b/security/access_denied_handler.rst index bb55d4f6c83..fec54c2e6c4 100644 --- a/security/access_denied_handler.rst +++ b/security/access_denied_handler.rst @@ -13,7 +13,7 @@ This interface defines one method called ``handle()`` where you can implement wh logic that should execute when access is denied for the current user (e.g. send a mail, log a message, or generally return a custom response):: - namespace AppBundle\Security; + namespace App\Security; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -30,7 +30,7 @@ mail, log a message, or generally return a custom response):: } } -If you're using the :ref:`default services.yml configuration `, +If you're using the :ref:`default services.yaml configuration `, you're done! Symfony will automatically know about your new service. You can then configure it under your firewall: @@ -38,26 +38,27 @@ configure it under your firewall: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml firewalls: # ... main: # ... - access_denied_handler: AppBundle\Security\AccessDeniedHandler + access_denied_handler: App\Security\AccessDeniedHandler .. code-block:: xml + - AppBundle\Security\AccessDeniedHandler + App\Security\AccessDeniedHandler .. code-block:: php - // app/config/security.php - use AppBundle\Security\AccessDeniedHandler; + // config/packages/security.php + use App\Security\AccessDeniedHandler; $container->loadFromExtension('security', [ 'firewalls' => [ diff --git a/security/acl.rst b/security/acl.rst index d5a2343e18d..ffbf16c7c27 100644 --- a/security/acl.rst +++ b/security/acl.rst @@ -4,248 +4,12 @@ How to Use Access Control Lists (ACLs) ====================================== -.. deprecated:: 3.4 +.. caution:: - ACL support was deprecated in Symfony 3.4 and will be removed in 4.0. Install - the `Symfony ACL bundle`_ if you want to keep using ACL. + ACL support was removed in Symfony 4.0. Install the `Symfony ACL bundle`_ + and refer to its documentation if you want to keep using ACL. -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. - -.. sidebar:: Alternatives to ACLs - - Using ACL's isn't trivial, and for simpler use cases, it may be overkill. - If your permission logic could be described by just writing some code (e.g. - to check if a Blog is owned by the current User), then consider using - :doc:`voters `. A voter is passed the object - being voted on, which you can use to make complex decisions and effectively - implement your own ACL. Enforcing authorization (e.g. the ``isGranted()`` - part) will look similar to what you see in this article, but your voter - class will handle the logic behind the scenes, instead of the ACL system. - -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 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: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - acl: - connection: default - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', [ - // ... - - 'acl' => [ - 'connection' => 'default', - ], - ]); - -.. note:: - - 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 -running the following command: - -.. code-block:: terminal - - $ php bin/console init:acl - -Getting Started ---------------- - -Coming back to the small example from the beginning, you can now implement -ACL for it. - -Once the ACL is created, you can grant access to objects by creating an -Access Control Entry (ACE) to solidify the relationship between the entity -and your user. - -Creating an ACL and Adding an ACE -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: php - - // src/AppBundle/Controller/BlogController.php - namespace AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Security\Acl\Domain\ObjectIdentity; - use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity; - use Symfony\Component\Security\Acl\Permission\MaskBuilder; - use Symfony\Component\Security\Core\Exception\AccessDeniedException; - - class BlogController extends Controller - { - // ... - - public function addCommentAction(Post $post) - { - $comment = new Comment(); - - // ... setup $form, and submit data - - if ($form->isSubmitted() && $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 - $tokenStorage = $this->get('security.token_storage'); - $user = $tokenStorage->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. - -.. tip:: - - The order in which ACEs are checked is significant. As a general rule, you - should place more specific entries at the beginning. - -Checking Access -~~~~~~~~~~~~~~~ - -.. code-block:: php - - // src/AppBundle/Controller/BlogController.php - - // ... - - class BlogController - { - // ... - - public function editCommentAction(Comment $comment) - { - $authorizationChecker = $this->get('security.authorization_checker'); - - // check for edit access - if (false === $authorizationChecker->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. - -.. note:: - - You can define up to 32 base permissions (depending on your OS, PHP might - vary between 30 and 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 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', 'AppBundle\Entity\User'); - $acl->insertObjectAce($identity, $mask); - -The user is now allowed to view, edit, delete, and undelete objects. + Consider using :doc:`security voters `, + the alternative to ACLs recommended by Symfony. .. _`Symfony ACL bundle`: https://github.com/symfony/acl-bundle -.. _`MongoDBAclBundle`: https://github.com/IamPersistent/MongoDBAclBundle diff --git a/security/acl_advanced.rst b/security/acl_advanced.rst deleted file mode 100644 index c38e330fb62..00000000000 --- a/security/acl_advanced.rst +++ /dev/null @@ -1,204 +0,0 @@ -.. index:: - single: Security; Advanced ACL concepts - -How to Use advanced ACL Concepts -================================ - -.. deprecated:: 3.4 - - ACL support was deprecated in Symfony 3.4 and will be removed in 4.0. Install - the `Symfony ACL bundle`_ if you want to keep using ACL. - -The aim of this article 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 make decisions of whether a person is allowed to - perform an action on a domain object or not. - -As indicated by the first point, one of the main capabilities of Symfony's -ACL system is a high-performance way of retrieving ACLs/ACEs. This is -extremely important since each ACL might have several ACEs, and inherit from -another ACL in a tree-like fashion. Therefore, no ORM is leveraged, instead -the default implementation interacts with your connection directly using Doctrine's -DBAL. - -Object Identities -~~~~~~~~~~~~~~~~~ - -The ACL system is completely decoupled from your domain objects. They don't -even have to be stored in the same database, or on the same server. In order -to achieve this decoupling, in the ACL system your objects are represented -through object identity objects. Every time you want to retrieve the ACL for a -domain object, the ACL system will first create an object identity from your -domain object, and then pass this object identity to the ACL provider for -further processing. - -Security Identities -~~~~~~~~~~~~~~~~~~~ - -This is analog to the object identity, but represents a user, or a role in -your application. Each role, or user has its own security identity. - -.. caution:: - - For users, the security identity is based on the username. This means that, - if for any reason, a user's username was to change, you must ensure its - security identity is updated too. The - :method:`MutableAclProvider::updateUserSecurityIdentity() ` - method is there to handle the update. - -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: - :class:`Symfony\\Component\\Security\\Acl\\Domain\\RoleSecurityIdentity` and - :class:`Symfony\\Component\\Security\\Acl\\Domain\\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. - -.. _security-acl-field_scope: - -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 article, and - it only applies to one specific object. - -Sometimes, you will find the need to apply an ACE only to a specific field of -the object. Suppose you want the ID only to be viewable by an administrator, -but not by your customer service. To solve this common problem, two more sub-scopes -have been added: - -- Class-Field-Scope: These entries apply to all objects with the same class, - but only to a specific field of the objects. -- Object-Field-Scope: These entries apply to a specific object, and only to a - specific field of that object. - -Pre-Authorization Decisions ---------------------------- - -For pre-authorization decisions, that is decisions made before any secure method (or -secure action) is invoked, the proven AccessDecisionManager service is used. -The AccessDecisionManager is also used for reaching authorization decisions based -on roles. Just like roles, the ACL system adds several new attributes which may be -used to check for different permissions. - -Built-in Permission Map -~~~~~~~~~~~~~~~~~~~~~~~ - -+------------------+----------------------------+-----------------------------+ -| Attribute | Intended Meaning | Integer Bitmasks | -+==================+============================+=============================+ -| VIEW | Whether someone is allowed | VIEW, EDIT, OPERATOR, | -| | to view the domain object. | MASTER, or OWNER | -+------------------+----------------------------+-----------------------------+ -| EDIT | Whether someone is allowed | EDIT, OPERATOR, MASTER, | -| | to make changes to the | or OWNER | -| | domain object. | | -+------------------+----------------------------+-----------------------------+ -| CREATE | Whether someone is allowed | CREATE, OPERATOR, MASTER, | -| | to create the domain | or OWNER | -| | object. | | -+------------------+----------------------------+-----------------------------+ -| DELETE | Whether someone is allowed | DELETE, OPERATOR, MASTER, | -| | to delete the domain | or OWNER | -| | object. | | -+------------------+----------------------------+-----------------------------+ -| UNDELETE | Whether someone is allowed | UNDELETE, OPERATOR, MASTER, | -| | to restore a previously | or OWNER | -| | deleted domain object. | | -+------------------+----------------------------+-----------------------------+ -| OPERATOR | Whether someone is allowed | OPERATOR, MASTER, or OWNER | -| | to perform all of the above| | -| | actions. | | -+------------------+----------------------------+-----------------------------+ -| MASTER | Whether someone is allowed | MASTER, or OWNER | -| | to perform all of the above| | -| | actions, and in addition is| | -| | allowed to grant | | -| | any of the above | | -| | permissions to others. | | -+------------------+----------------------------+-----------------------------+ -| OWNER | Whether someone owns the | OWNER | -| | domain object. An owner can| | -| | perform any of the above | | -| | actions *and* grant master | | -| | and owner permissions. | | -+------------------+----------------------------+-----------------------------+ - -Permission Attributes vs. Permission Bitmasks -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Attributes are used by the AccessDecisionManager, just like roles. Often, these -attributes represent in fact an aggregate of integer bitmasks. Integer bitmasks on -the other hand, are used by the ACL system internally to efficiently store your -users' permissions in the database, and perform access checks using extremely -fast bitmask operations. - -Extensibility -~~~~~~~~~~~~~ - -The above permission map is by no means static, and theoretically could be -completely replaced at will. However, it should cover most problems you -encounter, and for interoperability with other bundles, you are encouraged to -stick to the meaning envisaged for them. - -Post Authorization Decisions ----------------------------- - -Post authorization decisions are made after a secure method has been invoked, -and typically involve the domain object which is returned by such a method. -After invocation providers also allow to modify, or filter the domain object -before it is returned. - -Due to current limitations of the PHP language, there are no -post-authorization capabilities built 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 -:class:`Symfony\\Component\\Security\\Acl\\Domain\\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 one -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. - -.. _`Symfony ACL bundle`: https://github.com/symfony/acl-bundle -.. _`JMSSecurityExtraBundle`: https://github.com/schmittjoh/JMSSecurityExtraBundle diff --git a/security/api_key_authentication.rst b/security/api_key_authentication.rst deleted file mode 100644 index 00693f49ea6..00000000000 --- a/security/api_key_authentication.rst +++ /dev/null @@ -1,602 +0,0 @@ -.. index:: - single: Security; Custom Request Authenticator - -How to Authenticate Users with API Keys -======================================= - -.. tip:: - - Check out :doc:`/security/guard_authentication` for a simpler and more - flexible way to accomplish custom authentication tasks like this. - -Nowadays, it's quite usual to authenticate the user via an API key (when developing -a web service for instance). The API key is provided for every request and is -passed as a query string parameter or via an HTTP header. - -The API Key Authenticator -------------------------- - -Authenticating a user based on the Request information should be done via a -pre-authentication mechanism using the -:class:`Symfony\\Component\\Security\\Http\\Authentication\\SimplePreAuthenticatorInterface` -class. - -Your exact situation may differ, but in this example, a token is read -from an ``apikey`` query parameter, the proper username is loaded from that -value and then a User object is created:: - - // src/AppBundle/Security/ApiKeyAuthenticator.php - namespace AppBundle\Security; - - use AppBundle\Security\ApiKeyUserProvider; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken; - use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - use Symfony\Component\Security\Core\Exception\AuthenticationException; - use Symfony\Component\Security\Core\Exception\BadCredentialsException; - use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; - use Symfony\Component\Security\Core\User\UserProviderInterface; - use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface; - - class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface - { - public function createToken(Request $request, $providerKey) - { - // look for an apikey query parameter - $apiKey = $request->query->get('apikey'); - - // or if you want to use an "apikey" header, then do something like this: - // $apiKey = $request->headers->get('apikey'); - - if (!$apiKey) { - throw new BadCredentialsException(); - - // or to just skip api key authentication - // return null; - } - - return new PreAuthenticatedToken( - 'anon.', - $apiKey, - $providerKey - ); - } - - public function supportsToken(TokenInterface $token, $providerKey) - { - return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey; - } - - public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) - { - if (!$userProvider instanceof ApiKeyUserProvider) { - throw new \InvalidArgumentException( - sprintf( - 'The user provider must be an instance of ApiKeyUserProvider (%s was given).', - get_class($userProvider) - ) - ); - } - - $apiKey = $token->getCredentials(); - $username = $userProvider->getUsernameForApiKey($apiKey); - - if (!$username) { - // CAUTION: this message will be returned to the client - // (so don't put any un-trusted messages / error strings here) - throw new CustomUserMessageAuthenticationException( - sprintf('API Key "%s" does not exist.', $apiKey) - ); - } - - $user = $userProvider->loadUserByUsername($username); - - return new PreAuthenticatedToken( - $user, - $apiKey, - $providerKey, - $user->getRoles() - ); - } - } - -Once you've :ref:`configured ` everything, -you'll be able to authenticate by adding an ``apikey`` parameter to the query -string, like ``http://example.com/api/foo?apikey=37b51d194a7513e45b56f6524f2d51f2``. - -The authentication process has several steps, and your implementation will -probably differ: - -1. ``createToken()`` -~~~~~~~~~~~~~~~~~~~~ - -Early in the request cycle, Symfony calls ``createToken()``. Your job here -is to create a token object that contains all of the information from the -request that you need to authenticate the user (e.g. the ``apikey`` query -parameter). If that information is missing, throwing a -:class:`Symfony\\Component\\Security\\Core\\Exception\\BadCredentialsException` -will cause authentication to fail. You might want to return ``null`` instead -to just skip the authentication, so Symfony can fallback to another authentication -method, if any. - -.. caution:: - - In case you return ``null`` from your ``createToken()`` method, Symfony - passes this request to the next authentication provider. If you haven't - configured any other provider, enable the ``anonymous`` option in your - firewall. This way Symfony executes the anonymous authentication provider - and you'll get an ``AnonymousToken``. - -2. ``supportsToken()`` -~~~~~~~~~~~~~~~~~~~~~~ - -.. include:: _supportsToken.rst.inc - -3. ``authenticateToken()`` -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If ``supportsToken()`` returns ``true``, Symfony will now call ``authenticateToken()``. -One key part is the ``$userProvider``, which is an external class that helps -you load information about the user. You'll learn more about this next. - -In this specific example, the following things happen in ``authenticateToken()``: - -#. First, you use the ``$userProvider`` to somehow look up the ``$username`` that - corresponds to the ``$apiKey``; -#. Second, you use the ``$userProvider`` again to load or create a ``User`` - object for the ``$username``; -#. Finally, you create an *authenticated token* (i.e. a token with at least one - role) that has the proper roles and the User object attached to it. - -The goal is ultimately to use the ``$apiKey`` to find or create a ``User`` -object. *How* you do this (e.g. query a database) and the exact class for -your ``User`` object may vary. Those differences will be most notable in your -user provider. - -The User Provider -~~~~~~~~~~~~~~~~~ - -The ``$userProvider`` can be any user provider (see :doc:`/security/custom_provider`). -In this example, the ``$apiKey`` is used to somehow find the username for -the user. This work is done in a ``getUsernameForApiKey()`` method, which -is created entirely custom for this use-case (i.e. this isn't a method that's -used by Symfony's core user provider system). - -The ``$userProvider`` might look something like this:: - - // src/AppBundle/Security/ApiKeyUserProvider.php - namespace AppBundle\Security; - - use Symfony\Component\Security\Core\Exception\UnsupportedUserException; - use Symfony\Component\Security\Core\User\User; - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Security\Core\User\UserProviderInterface; - - class ApiKeyUserProvider implements UserProviderInterface - { - public function getUsernameForApiKey($apiKey) - { - // Look up the username based on the token in the database, via - // an API call, or do something entirely different - $username = ...; - - return $username; - } - - public function loadUserByUsername($username) - { - return new User( - $username, - null, - // the roles for the user - you may choose to determine - // these dynamically somehow based on the user - ['ROLE_API'] - ); - } - - public function refreshUser(UserInterface $user) - { - // this is used for storing authentication in the session - // but in this example, the token is sent in each request, - // so authentication can be stateless. Throwing this exception - // is proper to make things stateless - throw new UnsupportedUserException(); - } - - public function supportsClass($class) - { - return User::class === $class; - } - } - -Next, make sure this class is registered as a service. If you're using the -:ref:`default services.yml configuration `, -that happens automatically. A little later, you'll reference this service in -your :ref:`security.yml configuration `. - -.. note:: - - Read the dedicated article to learn - :doc:`how to create a custom user provider `. - -The logic inside ``getUsernameForApiKey()`` is up to you. You may somehow transform -the API key (e.g. ``37b51d``) into a username (e.g. ``jondoe``) by looking -up some information in a "token" database table. - -The same is true for ``loadUserByUsername()``. In this example, Symfony's core -:class:`Symfony\\Component\\Security\\Core\\User\\User` class is simply created. -This makes sense if you don't need to store any extra information on your -User object (e.g. ``firstName``). But if you do, you may instead have your *own* -user class which you create and populate here by querying a database. This -would allow you to have custom data on the ``User`` object. - -Finally, just make sure that ``supportsClass()`` returns ``true`` for User -objects with the same class as whatever user you return in ``loadUserByUsername()``. - -If your authentication is stateless like in this example (i.e. you expect -the user to send the API key with every request and so you don't save the -login to the session), then you can simply throw the ``UnsupportedUserException`` -exception in ``refreshUser()``. - -.. note:: - - If you *do* want to store authentication data in the session so that - the key doesn't need to be sent on every request, see :ref:`security-api-key-session`. - -Handling Authentication Failure -------------------------------- - -In order for your ``ApiKeyAuthenticator`` to correctly display a 401 -http status when either bad credentials or authentication fails you will -need to implement the :class:`Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationFailureHandlerInterface` on your -Authenticator. This will provide a method ``onAuthenticationFailure()`` which -you can use to create an error ``Response``:: - - // src/AppBundle/Security/ApiKeyAuthenticator.php - namespace AppBundle\Security; - - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Security\Core\Exception\AuthenticationException; - use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; - use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface; - - class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface - { - // ... - - public function onAuthenticationFailure(Request $request, AuthenticationException $exception) - { - return new Response( - // this contains information about *why* authentication failed - // use it, or return your own message - strtr($exception->getMessageKey(), $exception->getMessageData()), - 401 - ); - } - } - -.. _security-api-key-config: - -Configuration -------------- - -Once you have your ``ApiKeyAuthenticator`` all setup, you need to register -it as a service. If you're using the :ref:`default services.yml configuration `, -that happens automatically. - -The last step is to activate your authenticator and custom user provider in the -``firewalls`` section of your security configuration using the ``simple_preauth`` -and ``provider`` keys: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - providers: - api_key_user_provider: - id: AppBundle\Security\ApiKeyUserProvider - - firewalls: - main: - pattern: ^/api - stateless: true - simple_preauth: - authenticator: AppBundle\Security\ApiKeyAuthenticator - provider: api_key_user_provider - - .. code-block:: xml - - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - - // ... - use AppBundle\Security\ApiKeyAuthenticator; - use AppBundle\Security\ApiKeyUserProvider; - - $container->loadFromExtension('security', [ - 'providers' => [ - 'api_key_user_provider' => [ - 'id' => ApiKeyUserProvider::class, - ], - ], - 'firewalls' => [ - 'main' => [ - 'pattern' => '^/api', - 'stateless' => true, - 'simple_preauth' => [ - 'authenticator' => ApiKeyAuthenticator::class, - ], - 'provider' => 'api_key_user_provider', - ], - ], - ]); - -If you have defined ``access_control``, make sure to add a new entry: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - access_control: - - { path: '^/api', roles: ROLE_API } - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', [ - 'access_control' => [ - [ - 'path' => '^/api', - 'roles' => 'ROLE_API', - ], - ], - ]); - -That's it! Now, your ``ApiKeyAuthenticator`` should be called at the beginning -of each request and your authentication process will take place. - -The ``stateless`` configuration parameter prevents Symfony from trying to -store the authentication information in the session, which isn't necessary -since the client will send the ``apikey`` on each request. If you *do* need -to store authentication in the session, keep reading! - -.. _security-api-key-session: - -Storing Authentication in the Session -------------------------------------- - -So far, this article has described a situation where some sort of authentication -token is sent on every request. But in some situations (like an OAuth flow), -the token may be sent on only *one* request. In this case, you will want to -authenticate the user and store that authentication in the session so that -the user is automatically logged in for every subsequent request. - -To make this work, first remove the ``stateless`` key from your firewall -configuration or set it to ``false``: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - firewalls: - secured_area: - pattern: ^/api - stateless: false - # ... - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - - // ... - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'secured_area' => [ - 'pattern' => '^/api', - 'stateless' => false, - // ... - ], - ], - ]); - -Even though the token is being stored in the session, the credentials - in this -case the API key (i.e. ``$token->getCredentials()``) - are not stored in the session -for security reasons. To take advantage of the session, update ``ApiKeyAuthenticator`` -to see if the stored token has a valid User object that can be used:: - - // src/AppBundle/Security/ApiKeyAuthenticator.php - - // ... - class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface - { - // ... - public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) - { - if (!$userProvider instanceof ApiKeyUserProvider) { - throw new \InvalidArgumentException( - sprintf( - 'The user provider must be an instance of ApiKeyUserProvider (%s was given).', - get_class($userProvider) - ) - ); - } - - $apiKey = $token->getCredentials(); - $username = $userProvider->getUsernameForApiKey($apiKey); - - // User is the Entity which represents your user - $user = $token->getUser(); - if ($user instanceof User) { - return new PreAuthenticatedToken( - $user, - $apiKey, - $providerKey, - $user->getRoles() - ); - } - - if (!$username) { - // this message will be returned to the client - throw new CustomUserMessageAuthenticationException( - sprintf('API Key "%s" does not exist.', $apiKey) - ); - } - - $user = $userProvider->loadUserByUsername($username); - - return new PreAuthenticatedToken( - $user, - $apiKey, - $providerKey, - $user->getRoles() - ); - } - // ... - } - -Storing authentication information in the session works like this: - -#. At the end of each request, Symfony serializes the token object (returned - from ``authenticateToken()``), which also serializes the ``User`` object - (since it's set on a property on the token); -#. On the next request the token is deserialized and the deserialized ``User`` - object is passed to the ``refreshUser()`` function of the user provider. - -The second step is the important one: Symfony calls ``refreshUser()`` and passes -you the user object that was serialized in the session. If your users are -stored in the database, then you may want to re-query for a fresh version -of the user to make sure it's not out-of-date. But regardless of your requirements, -``refreshUser()`` should now return the User object:: - - // src/AppBundle/Security/ApiKeyUserProvider.php - - // ... - class ApiKeyUserProvider implements UserProviderInterface - { - // ... - - public function refreshUser(UserInterface $user) - { - // $user is the User that you set in the token inside authenticateToken() - // after it has been deserialized from the session - - // you might use $user to query the database for a fresh user - // $id = $user->getId(); - // use $id to make a query - - // if you are *not* reading from a database and are just creating - // a User object (like in this example), you can just return it - return $user; - } - } - -.. note:: - - You'll also want to make sure that your ``User`` object is being serialized - correctly. If your ``User`` object has private properties, PHP can't serialize - those. In this case, you may get back a User object that has a ``null`` - value for each property. For an example, see :doc:`/security/entity_provider`. - -Only Authenticating for Certain URLs ------------------------------------- - -This article has assumed that you want to look for the ``apikey`` authentication -on *every* request. But in some situations (like an OAuth flow), you only -really need to look for authentication information once the user has reached -a certain URL (e.g. the redirect URL in OAuth). - -Fortunately, handling this situation is easy: just check to see what the -current URL is before creating the token in ``createToken()``:: - - // src/AppBundle/Security/ApiKeyAuthenticator.php - - // ... - use Symfony\Component\HttpFoundation\Request; - - class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface - { - - public function createToken(Request $request, $providerKey) - { - // set the only URL where we should look for auth information - // and only return the token if we're at that URL - $targetUrl = '/login/check'; - if ($request->getPathInfo() !== $targetUrl) { - return; - } - - // ... - } - } - -That's it! Have fun! diff --git a/security/auth_providers.rst b/security/auth_providers.rst new file mode 100644 index 00000000000..69fd0abaaa2 --- /dev/null +++ b/security/auth_providers.rst @@ -0,0 +1,232 @@ +Built-in Authentication Providers +================================= + +If you need to add authentication to your app, we recommend using +:doc:`Guard authentication ` because it gives you +full control over the process. + +But, Symfony also offers a number of built-in authentication providers: systems +that are easier to implement, but harder to customize. If your authentication +use-case matches one of these exactly, they're a great option: + +.. toctree:: + :hidden: + + form_login + json_login_setup + +* :doc:`form_login ` +* :ref:`http_basic ` +* :doc:`LDAP via HTTP Basic or Form Login ` +* :doc:`json_login ` +* :ref:`X.509 Client Certificate Authentication (x509) ` +* :ref:`REMOTE_USER Based Authentication (remote_user) ` + +.. _security-http_basic: + +HTTP Basic Authentication +------------------------- + +`HTTP Basic authentication`_ asks credentials (username and password) using a dialog +in the browser. The credentials are sent without any hashing or encryption, so +it's recommended to use it with HTTPS. + +To support HTTP Basic authentication, add the ``http_basic`` key to your firewall: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + http_basic: + realm: Secured Area + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + // ... + + 'firewalls' => [ + 'main' => [ + 'http_basic' => [ + 'realm' => 'Secured Area', + ], + ], + ], + ]); + +That's it! Symfony will now be listening for any HTTP basic authentication data. +To load user information, it will use your configured :doc:`user provider `. + +Note: you cannot use the :ref:`log out ` with ``http_basic``. +Even if you log out, your browser "remembers" your credentials and will send them +on every request. + +.. _security-x509: + +X.509 Client Certificate Authentication +--------------------------------------- + +When using client certificates, your web server 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: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + x509: + provider: your_user_provider + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + // ... + + 'firewalls' => [ + 'main' => [ + // ... + 'x509' => [ + '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 +:class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\PreAuthenticatedToken`. +You can override these by setting the ``user`` and the ``credentials`` keys +in the x509 firewall configuration respectively. + +.. _security-pre-authenticated-user-provider-note: + +.. note:: + + 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: + + * :doc:`/security/user_provider` + +.. _security-remote_user: + +REMOTE_USER Based Authentication +-------------------------------- + +A lot of authentication modules, like ``auth_kerb`` for Apache, provide the username +using the ``REMOTE_USER`` environment variable. This variable can be trusted by +the application since the authentication happened before the request reached it. + +To configure Symfony using the ``REMOTE_USER`` environment variable, enable the +corresponding firewall in your security configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + # ... + remote_user: + provider: your_user_provider + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + 'firewalls' => [ + 'main' => [ + 'remote_user' => [ + 'provider' => 'your_user_provider', + ], + ], + ], + ]); + +The firewall will then provide the ``REMOTE_USER`` environment variable to +your user provider. You can change the variable name used by setting the ``user`` +key in the ``remote_user`` firewall configuration. + +.. note:: + + Just like for X509 authentication, you will need to configure a "user provider". + See :ref:`the previous note ` + for more information. + +.. _`HTTP Basic authentication`: https://en.wikipedia.org/wiki/Basic_access_authentication diff --git a/security/csrf.rst b/security/csrf.rst new file mode 100644 index 00000000000..45520e4bf2b --- /dev/null +++ b/security/csrf.rst @@ -0,0 +1,162 @@ +.. index:: + single: CSRF; CSRF protection + +How to Implement 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. + +CSRF protection works by adding a hidden field to your form that contains a +value that only you and your user know. This ensures that the user - not some +other entity - is submitting the given data. + +Before using the CSRF protection, install it in your project: + +.. code-block:: terminal + + $ composer require symfony/security-csrf + +Then, enable/disable the CSRF protection with the ``csrf_protection`` option +(see the :ref:`CSRF configuration reference ` +for more information): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + csrf_protection: ~ + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + $container->loadFromExtension('framework', [ + 'csrf_protection' => null, + ]); + +The tokens used for CSRF protection are meant to be different for every user and +they are stored in the session. That's why a session is started automatically as +soon as you render a form with CSRF protection. + +.. _caching-pages-that-contain-csrf-protected-forms: + +Moreover, this means that you cannot fully cache pages that include CSRF +protected forms. As an alternative, you can: + +* Embed the form inside an uncached :doc:`ESI fragment ` and + cache the rest of the page contents; +* Cache the entire page and load the form via an uncached AJAX request; +* Cache the entire page and use :doc:`hinclude.js ` to + load just the CSRF token with an uncached AJAX request and replace the form + field value with it. + +CSRF Protection in Symfony Forms +-------------------------------- + +Forms created with the Symfony Form component include CSRF tokens by default +and Symfony checks them automatically, so you don't have to do anything to be +protected against CSRF attacks. + +.. _form-csrf-customization: + +By default Symfony adds the CSRF token in a hidden field called ``_token``, but +this can be customized on a form-by-form basis:: + + // ... + use App\Entity\Task; + use Symfony\Component\OptionsResolver\OptionsResolver; + + class TaskType extends AbstractType + { + // ... + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Task::class, + // enable/disable CSRF protection for this form + 'csrf_protection' => true, + // the name of the hidden HTML field that stores the token + 'csrf_field_name' => '_token', + // an arbitrary string used to generate the value of the token + // using a different string for each form improves its security + 'csrf_token_id' => 'task_item', + ]); + } + + // ... + } + +You can also customize the rendering of the CSRF form field creating a custom +:doc:`form theme ` and using ``csrf_token`` as the prefix of +the field (e.g. define ``{% block csrf_token_widget %} ... {% endblock %}`` to +customize the entire form field contents). + +CSRF Protection in Login Forms +------------------------------ + +See :doc:`/security/form_login_setup` for a login form that is protected from +CSRF attacks. You can also configure the +:ref:`CSRF protection for the logout action `. + +.. _csrf-protection-in-html-forms: + +Generating and Checking CSRF Tokens Manually +-------------------------------------------- + +Although Symfony Forms provide automatic CSRF protection by default, you may +need to generate and check CSRF tokens manually for example when using regular +HTML forms not managed by the Symfony Form component. + +Consider a simple HTML form created to allow deleting items. First, use the +:ref:`csrf_token() Twig function ` to +generate a CSRF token in the template and store it as a hidden form field: + +.. code-block:: html+twig + +
+ {# the argument of csrf_token() is an arbitrary string used to generate the token #} + + + +
+ +Then, get the value of the CSRF token in the controller action and use the +:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::isCsrfTokenValid` +to check its validity:: + + use Symfony\Component\HttpFoundation\Request; + // ... + + public function delete(Request $request) + { + $submittedToken = $request->request->get('token'); + + // 'delete-item' is the same value used in the template to generate the token + if ($this->isCsrfTokenValid('delete-item', $submittedToken)) { + // ... do something, like deleting an object + } + } + +.. _`Cross-site request forgery`: http://en.wikipedia.org/wiki/Cross-site_request_forgery diff --git a/security/csrf_in_login_form.rst b/security/csrf_in_login_form.rst deleted file mode 100644 index 5df9ddfac5c..00000000000 --- a/security/csrf_in_login_form.rst +++ /dev/null @@ -1,210 +0,0 @@ -.. index:: - single: Security; CSRF Protection in the Login Form - -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. - -.. note:: - - Login CSRF attacks are a bit less well-known. See `Forging Login Requests`_ - if you're curious about more details. - -Configuring CSRF Protection ---------------------------- - -First, make sure that the CSRF protection is enabled in the main configuration -file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - # ... - csrf_protection: ~ - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', [ - 'csrf_protection' => null, - ]); - -Then, the security component needs a CSRF token provider. You can set this to -use the default provider available in the security component: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - firewalls: - secured_area: - # ... - form_login: - # ... - csrf_token_generator: security.csrf.token_manager - - .. code-block:: xml - - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', [ - // ... - - 'firewalls' => [ - 'secured_area' => [ - // ... - 'form_login' => [ - // ... - 'csrf_token_generator' => 'security.csrf.token_manager', - ], - ], - ], - ]); - -The Security component can be configured further, but this is all information -it needs to be able to use CSRF in the login form. - -.. tip:: - - If you're using a :doc:`Guard Authenticator `, - you'll need to validate the CSRF token manually inside of that class. See - :ref:`guard-csrf-protection` for details. - -.. _csrf-login-template: - -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: - -.. code-block:: html+twig - - {# src/AppBundle/Resources/views/Security/login.html.twig #} - - {# ... #} -
- {# ... the login fields #} - - - - -
- -After this, you have protected your login form against CSRF attacks. - -.. tip:: - - You can change the name of the field by setting ``csrf_parameter`` and change - the token ID by setting ``csrf_token_id`` in your configuration: - - .. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - firewalls: - secured_area: - # ... - form_login: - # ... - csrf_parameter: _csrf_security_token - csrf_token_id: a_private_string - - .. code-block:: xml - - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', [ - // ... - - 'firewalls' => [ - 'secured_area' => [ - // ... - 'form_login' => [ - // ... - 'csrf_parameter' => '_csrf_security_token', - 'csrf_token_id' => 'a_private_string', - ], - ], - ], - ]); - -.. _`Cross-site request forgery`: https://en.wikipedia.org/wiki/Cross-site_request_forgery -.. _`Forging Login Requests`: https://en.wikipedia.org/wiki/Cross-site_request_forgery#Forging_login_requests diff --git a/security/custom_authentication_provider.rst b/security/custom_authentication_provider.rst index 9b9b83edc61..a1732e8f08b 100644 --- a/security/custom_authentication_provider.rst +++ b/security/custom_authentication_provider.rst @@ -4,21 +4,19 @@ How to Create a custom Authentication Provider ============================================== -.. tip:: - - Creating a custom authentication system is hard, and this article will walk - you through that process. But depending on your needs, you may be able - to solve your problem in a simpler manner, or via a community bundle: - - * :doc:`/security/guard_authentication` - * :doc:`/security/custom_password_authenticator` - * :doc:`/security/api_key_authentication` - * To authenticate via OAuth using a third-party service such as Google, Facebook - or Twitter, try using the `HWIOAuthBundle`_ community bundle. - -If you have read the article on :doc:`/security`, you understand the -distinction Symfony makes between authentication and authorization in the -implementation of security. This article discusses the core classes involved +.. caution:: + + Creating a custom authentication system is hard, and almost definitely + **not** needed. Instead, see :doc:`/security/guard_authentication` for a + simple way to create an authentication system you will love. Do **not** + keep reading unless you want to learn the lowest level details of + authentication. + +Symfony provides support for the most +:doc:`common authentication mechanisms `. However, your +app may need to integrated with some proprietary single-sign-on system or some +legacy authentication mechanism. In those cases you could create a custom +authentication provider. This article 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 @@ -61,8 +59,8 @@ 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; + // src/Security/Authentication/Token/WsseUserToken.php + namespace App\Security\Authentication\Token; use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; @@ -99,24 +97,22 @@ The Listener Next, you need a listener to listen on the firewall. The listener is responsible for fielding requests to the firewall and calling the authentication -provider. A listener must be an instance of -:class:`Symfony\\Component\\Security\\Http\\Firewall\\ListenerInterface`. +provider. Listener is a callable, so you have to implement an ``__invoke()`` method. A security listener should handle the -:class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent` event, and +:class:`Symfony\\Component\\HttpKernel\\Event\\RequestEvent` event, and set an authenticated token in the token storage if successful:: - // src/AppBundle/Security/Firewall/WsseListener.php - namespace AppBundle\Security\Firewall; + // src/Security/Firewall/WsseListener.php + namespace App\Security\Firewall; - use AppBundle\Security\Authentication\Token\WsseUserToken; + use App\Security\Authentication\Token\WsseUserToken; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\HttpKernel\Event\GetResponseEvent; + use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; - use Symfony\Component\Security\Http\Firewall\ListenerInterface; - class WsseListener implements ListenerInterface + class WsseListener { protected $tokenStorage; protected $authenticationManager; @@ -127,7 +123,7 @@ set an authenticated token in the token storage if successful:: $this->authenticationManager = $authenticationManager; } - public function handle(GetResponseEvent $event) + public function __invoke(RequestEvent $event) { $request = $event->getRequest(); @@ -199,15 +195,14 @@ 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; + // src/Security/Authentication/Provider/WsseProvider.php + namespace App\Security\Authentication\Provider; - use AppBundle\Security\Authentication\Token\WsseUserToken; + use App\Security\Authentication\Token\WsseUserToken; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; - use Symfony\Component\Security\Core\Exception\NonceExpiredException; use Symfony\Component\Security\Core\User\UserProviderInterface; class WsseProvider implements AuthenticationProviderInterface @@ -259,7 +254,9 @@ the ``PasswordDigest`` header value matches with the user's password:: // Validate that the nonce is *not* in cache // if it is, this could be a replay attack if ($cacheItem->isHit()) { - throw new NonceExpiredException('Previously used nonce detected'); + // In a real world application you should throw a custom + // exception extending the AuthenticationException + throw new AuthenticationException('Previously used nonce detected'); } // Store the item in cache for 5 minutes @@ -286,13 +283,6 @@ the ``PasswordDigest`` header value matches with the user's password:: provider for the given token. In the case of multiple providers, the authentication manager will then move to the next provider in the list. -.. note:: - - While the :phpfunction:`hash_equals` function was introduced in PHP 5.6, - you are safe to use it with any PHP version in your Symfony application. In - PHP versions prior to 5.6, `Symfony Polyfill`_ (which is included in - Symfony) will define the function for you. - The Factory ----------- @@ -304,11 +294,11 @@ provider and any configuration options available for it. First, you must create a class which implements :class:`Symfony\\Bundle\\SecurityBundle\\DependencyInjection\\Security\\Factory\\SecurityFactoryInterface`:: - // src/AppBundle/DependencyInjection/Security/Factory/WsseFactory.php - namespace AppBundle\DependencyInjection\Security\Factory; + // src/DependencyInjection/Security/Factory/WsseFactory.php + namespace App\DependencyInjection\Security\Factory; - use AppBundle\Security\Authentication\Provider\WsseProvider; - use AppBundle\Security\Firewall\WsseListener; + use App\Security\Authentication\Provider\WsseProvider; + use App\Security\Firewall\WsseListener; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\ChildDefinition; @@ -392,42 +382,42 @@ 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 may not exist yet: ``AppBundle\Security\Authentication\Provider\WsseProvider`` and -``AppBundle\Security\Firewall\WsseListener``. It's time to define those services. +to service ids that may not exist yet: ``App\Security\Authentication\Provider\WsseProvider`` and +``App\Security\Firewall\WsseListener``. It's time to define those services. .. configuration-block:: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # ... - AppBundle\Security\Authentication\Provider\WsseProvider: + App\Security\Authentication\Provider\WsseProvider: arguments: $cachePool: '@cache.app' public: false - AppBundle\Security\Firewall\WsseListener: + App\Security\Firewall\WsseListener: arguments: ['@security.token_storage', '@security.authentication.manager'] public: false .. code-block:: xml - + - - @@ -438,9 +428,9 @@ to service ids that may not exist yet: ``AppBundle\Security\Authentication\Provi .. code-block:: php - // app/config/services.php - use AppBundle\Security\Authentication\Provider\WsseProvider; - use AppBundle\Security\Firewall\WsseListener; + // config/services.php + use App\Security\Authentication\Provider\WsseProvider; + use App\Security\Firewall\WsseListener; use Symfony\Component\DependencyInjection\Reference; $container->register(WsseProvider::class) @@ -455,24 +445,23 @@ to service ids that may not exist yet: ``AppBundle\Security\Authentication\Provi ->setPublic(false); Now that your services are defined, tell your security context about your -factory in your bundle class:: +factory in the kernel:: - // src/AppBundle/AppBundle.php - namespace AppBundle; + // src/Kernel.php + namespace App; - use AppBundle\DependencyInjection\Security\Factory\WsseFactory; - use Symfony\Component\DependencyInjection\ContainerBuilder; - use Symfony\Component\HttpKernel\Bundle\Bundle; + use App\DependencyInjection\Security\Factory\WsseFactory; + // ... - class AppBundle extends Bundle + class Kernel extends BaseKernel { 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. @@ -481,7 +470,7 @@ You are finished! You can now define parts of your app as under WSSE protection. .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -493,7 +482,7 @@ You are finished! You can now define parts of your app as under WSSE protection. .. code-block:: xml - + loadFromExtension('security', [ // ... @@ -567,7 +556,7 @@ 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:: - use AppBundle\Security\Authentication\Provider\WsseProvider; + use App\Security\Authentication\Provider\WsseProvider; class WsseFactory implements SecurityFactoryInterface { @@ -597,7 +586,7 @@ set to any desirable value per firewall. .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -609,7 +598,7 @@ set to any desirable value per firewall. .. code-block:: xml - + loadFromExtension('security', [ // ... @@ -646,7 +635,6 @@ set to any desirable value per firewall. 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. -.. _`HWIOAuthBundle`: https://github.com/hwi/HWIOAuthBundle + .. _`WSSE`: http://www.xml.com/pub/a/2003/12/17/dive.html .. _`nonce`: https://en.wikipedia.org/wiki/Cryptographic_nonce -.. _`Symfony Polyfill`: https://github.com/symfony/polyfill diff --git a/security/custom_password_authenticator.rst b/security/custom_password_authenticator.rst deleted file mode 100644 index 547baa0c1f4..00000000000 --- a/security/custom_password_authenticator.rst +++ /dev/null @@ -1,227 +0,0 @@ -.. index:: - single: Security; Custom Password Authenticator - -How to Create a Custom Form Password Authenticator -================================================== - -.. tip:: - - Check out :doc:`/security/guard_authentication` for a simpler and more - flexible way to accomplish custom authentication tasks like this. - -Imagine you want to allow access to your website only between 2pm and 4pm -UTC. In this article, you'll learn how to do this for a login form (i.e. where -your user submits their username and password). - -The Password Authenticator --------------------------- - -First, create a new class that implements -:class:`Symfony\\Component\\Security\\Http\\Authentication\\SimpleFormAuthenticatorInterface`. -Eventually, this will allow you to create custom logic for authenticating -the user:: - - // src/AppBundle/Security/TimeAuthenticator.php - namespace AppBundle\Security; - - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; - use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; - use Symfony\Component\Security\Core\Exception\BadCredentialsException; - use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; - use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; - use Symfony\Component\Security\Core\User\UserProviderInterface; - use Symfony\Component\Security\Http\Authentication\SimpleFormAuthenticatorInterface; - - class TimeAuthenticator implements SimpleFormAuthenticatorInterface - { - private $encoder; - - public function __construct(UserPasswordEncoderInterface $encoder) - { - $this->encoder = $encoder; - } - - public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) - { - try { - $user = $userProvider->loadUserByUsername($token->getUsername()); - } catch (UsernameNotFoundException $exception) { - // CAUTION: this message will be returned to the client - // (so don't put any un-trusted messages / error strings here) - throw new CustomUserMessageAuthenticationException('Invalid username or password'); - } - - $currentUser = $token->getUser(); - - if ($currentUser instanceof UserInterface) { - if ($currentUser->getPassword() !== $user->getPassword()) { - throw new BadCredentialsException('The credentials were changed from another session.'); - } - } else { - if ('' === ($givenPassword = $token->getCredentials())) { - throw new BadCredentialsException('The given password cannot be empty.'); - } - if (!$this->encoder->isPasswordValid($user, $givenPassword)) { - throw new BadCredentialsException('The given password is invalid.'); - } - } - - $currentHour = date('G'); - if ($currentHour < 14 || $currentHour > 16) { - // CAUTION: this message will be returned to the client - // (so don't put any un-trusted messages / error strings here) - throw new CustomUserMessageAuthenticationException( - 'You can only log in between 2 and 4!', - [], // Message Data - 412 // HTTP 412 Precondition Failed - ); - } - - return new UsernamePasswordToken( - $user, - $user->getPassword(), - $providerKey, - $user->getRoles() - ); - } - - public function supportsToken(TokenInterface $token, $providerKey) - { - return $token instanceof UsernamePasswordToken - && $token->getProviderKey() === $providerKey; - } - - public function createToken(Request $request, $username, $password, $providerKey) - { - return new UsernamePasswordToken($username, $password, $providerKey); - } - } - -How it Works ------------- - -Great! Now you just need to setup some :ref:`security-password-authenticator-config`. -But first, you can find out more about what each method in this class does. - -1) ``createToken()`` -~~~~~~~~~~~~~~~~~~~~ - -When Symfony begins handling a request, ``createToken()`` is called, where -you create a :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface` -object that contains whatever information you need in ``authenticateToken()`` -to authenticate the user (e.g. the username and password). - -Whatever token object you create here will be passed to you later in ``authenticateToken()``. - -2) ``supportsToken()`` -~~~~~~~~~~~~~~~~~~~~~~ - -.. include:: _supportsToken.rst.inc - -3) ``authenticateToken()`` -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If ``supportsToken()`` returns ``true``, Symfony will now call ``authenticateToken()``. -Your job here is to check that the token is allowed to log in by first -getting the ``User`` object via the user provider and then, by checking the password -and the current time. - -.. note:: - - The "flow" of how you get the ``User`` object and determine whether or not - the token is valid (e.g. checking the password), may vary based on your - requirements. - -Ultimately, your job is to return a *new* token object that is "authenticated" -(i.e. it has at least 1 role set on it) and which has the ``User`` object -inside of it. - -Inside this method, the password encoder is needed to check the password's validity:: - - $isPasswordValid = $this->encoder->isPasswordValid($user, $token->getCredentials()); - -This is a service that is already available in Symfony and it uses the password algorithm -that is configured in the security configuration (e.g. ``security.yml``) under -the ``encoders`` key. Below, you'll see how to inject that into the ``TimeAuthenticator``. - -.. _security-password-authenticator-config: - -Configuration -------------- - -Now, make sure your ``TimeAuthenticator`` is registered as a service. If you're -using the :ref:`default services.yml configuration `, -that happens automatically. - -Finally, activate the service in the ``firewalls`` section of the security configuration -using the ``simple_form`` key: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - firewalls: - secured_area: - pattern: ^/admin - # ... - simple_form: - authenticator: AppBundle\Security\TimeAuthenticator - check_path: login_check - login_path: login - - .. code-block:: xml - - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - - // ... - use AppBundle\Security\TimeAuthenticator; - - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'secured_area' => [ - 'pattern' => '^/admin', - 'simple_form' => [ - 'provider' => ..., - 'authenticator' => AppBundle\Security\TimeAuthenticator::class, - 'check_path' => 'login_check', - 'login_path' => 'login', - ], - ], - ], - ]); - -The ``simple_form`` key has the same options as the normal ``form_login`` -option, but with the additional ``authenticator`` key that points to the -new service. For details, see :ref:`reference-security-firewall-form-login`. - -If creating a login form in general is new to you or you don't understand -the ``check_path`` or ``login_path`` options, see :doc:`/security/form_login`. diff --git a/security/custom_provider.rst b/security/custom_provider.rst deleted file mode 100644 index f4e69c68132..00000000000 --- a/security/custom_provider.rst +++ /dev/null @@ -1,353 +0,0 @@ -.. index:: - single: Security; User Provider - -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 four user providers: ``memory``, ``entity``, -``ldap`` and ``chain``. In this article 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 :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. -The methods in this interface should therefore be defined in the custom user -class: :method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getRoles`, -:method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getPassword`, -:method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getSalt`, -:method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getUsername`, -:method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::eraseCredentials`. -It may also be useful to implement the -:class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface` interface, -which defines a method to check if the user is equal to the current user. This -interface requires an :method:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface::isEqualTo` -method. - -This is how your ``WebserviceUser`` class looks in action:: - - // src/AppBundle/Security/User/WebserviceUser.php - namespace AppBundle\Security\User; - - use Symfony\Component\Security\Core\User\EquatableInterface; - use Symfony\Component\Security\Core\User\UserInterface; - - 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 -:class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`, -which requires three methods to be defined: ``loadUserByUsername($username)``, -``refreshUser(UserInterface $user)``, and ``supportsClass($class)``. For -more details, see :class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`. - -Here's an example of how this might look:: - - // src/AppBundle/Security/User/WebserviceUserProvider.php - namespace AppBundle\Security\User; - - use AppBundle\Security\User\WebserviceUser; - use Symfony\Component\Security\Core\Exception\UnsupportedUserException; - use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Security\Core\User\UserProviderInterface; - - class WebserviceUserProvider implements UserProviderInterface - { - public function loadUserByUsername($username) - { - return $this->fetchUser($username); - } - - public function refreshUser(UserInterface $user) - { - if (!$user instanceof WebserviceUser) { - throw new UnsupportedUserException( - sprintf('Instances of "%s" are not supported.', get_class($user)) - ); - } - - $username = $user->getUsername(); - - return $this->fetchUser($username); - } - - public function supportsClass($class) - { - return WebserviceUser::class === $class; - } - - private function fetchUser($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) - ); - } - } - -Create a Service for the User Provider --------------------------------------- - -Now you make the user provider available as a service. If you're using the -:ref:`default services.yml configuration `, -this happens automatically. - -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. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - providers: - webservice: - id: AppBundle\Security\User\WebserviceUserProvider - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - use AppBundle\Security\User\WebserviceUserProvider; - - $container->loadFromExtension('security', [ - // ... - - 'providers' => [ - 'webservice' => [ - 'id' => WebserviceUserProvider::class, - ], - ], - ]); - -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: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - encoders: - AppBundle\Security\User\WebserviceUser: bcrypt - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - use AppBundle\Security\User\WebserviceUser; - - $container->loadFromExtension('security', [ - // ... - - 'encoders' => [ - WebserviceUser::class => 'bcrypt', - ], - // ... - ]); - -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, it's encoded using this algorithm and the result -is compared to the hashed password returned by your ``getPassword()`` method. - -.. sidebar:: Specifics on how Passwords are Encoded - - Symfony uses a specific method to combine the salt and encode the password - before comparing it to your encoded password. If ``getSalt()`` returns - nothing, then the submitted password is simply encoded using the algorithm - you specify in ``security.yml``. If a salt *is* specified, then the following - value is created and *then* hashed via the algorithm:: - - $password.'{'.$salt.'}' - - If your external users have their passwords salted via a different method, - then you'll need to do a bit more work so that Symfony properly encodes - the password. That is beyond the scope of this article, but would include - sub-classing ``MessageDigestPasswordEncoder`` and overriding the - ``mergePasswordAndSalt()`` method. - - Additionally, you can configure the details of the algorithm used to hash - passwords. In this example, the application sets explicitly the cost of - the bcrypt hashing: - - .. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - encoders: - AppBundle\Security\User\WebserviceUser: - algorithm: bcrypt - cost: 12 - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - use AppBundle\Security\User\WebserviceUser; - - $container->loadFromExtension('security', [ - // ... - - 'encoders' => [ - WebserviceUser::class => [ - 'algorithm' => 'bcrypt', - 'cost' => 12, - ], - ], - ]); diff --git a/security/entity_provider.rst b/security/entity_provider.rst deleted file mode 100644 index 0e3573776e8..00000000000 --- a/security/entity_provider.rst +++ /dev/null @@ -1,553 +0,0 @@ -.. index:: - single: Security; User provider - single: Security; Entity provider - -How to Load Security Users from the Database (the Entity Provider) -================================================================== - -Symfony's security system can load security users from anywhere - like a -database, via Active Directory or an OAuth server. This article will show -you how to load your users from the database via a Doctrine entity. - -Introduction ------------- - -.. tip:: - - Before you start, you should check out `FOSUserBundle`_. This external - bundle allows you to load users from the database (like you'll learn here) - *and* gives you built-in routes & controllers for things like login, - registration and forgot password. But, if you need to heavily customize - your user system *or* if you want to learn how things work, this tutorial - is even better. - -Loading users via a Doctrine entity has 2 basic steps: - -#. :ref:`Create your User entity ` -#. :ref:`Configure security.yml to load from your entity ` - -Afterwards, you can learn more about :ref:`forbidding inactive users `, -:ref:`using a custom query ` -and :ref:`user serialization to the session ` - -.. _security-crete-user-entity: -.. _the-data-model: - -1) Create your User Entity --------------------------- - -For this article, suppose that you already have a ``User`` entity inside an -``AppBundle`` with the following fields: ``id``, ``username``, ``password``, -``email`` and ``isActive``:: - - // src/AppBundle/Entity/User.php - namespace AppBundle\Entity; - - use Doctrine\ORM\Mapping as ORM; - use Symfony\Component\Security\Core\User\UserInterface; - - /** - * @ORM\Table(name="app_users") - * @ORM\Entity(repositoryClass="AppBundle\Repository\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=254, 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('', true)); - } - - public function getUsername() - { - return $this->username; - } - - public function getSalt() - { - // you *may* need a real salt depending on your encoder - // see section on salt below - return null; - } - - public function getPassword() - { - return $this->password; - } - - public function getRoles() - { - return ['ROLE_USER']; - } - - public function eraseCredentials() - { - } - - /** @see \Serializable::serialize() */ - public function serialize() - { - return serialize([ - $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, ['allowed_classes' => false]); - } - } - -To make things shorter, some of the getter and setter methods aren't shown. -But you can generate these manually or with your own IDE. - -.. caution:: - - In the example above, the User entity's table name is "app_users" because - "USER" is a SQL reserved word. If you wish to call your table name "user", - `it must be quoted with backticks`_ to avoid errors. The annotation should - look like ``@ORM\Table(name="`user`")``. - -Next, make sure to :ref:`create the database table `: - -.. code-block:: terminal - - $ php bin/console doctrine:schema:update --force - -What's this UserInterface? -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -So far, this is just a normal entity. But to use this class in the -security system, it must implement -:class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. This -forces the class to have the five following methods: - -* :method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getRoles` -* :method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getPassword` -* :method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getSalt` -* :method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getUsername` -* :method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::eraseCredentials` - -To learn more about each of these, see :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. - -.. caution:: - - The ``eraseCredentials()`` method is only meant to clean up possibly stored - plain text passwords (or similar credentials). Be careful what to erase - if your user class is also mapped to a database as the modified object - will likely be persisted during the request. - -What do the serialize and unserialize Methods do? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -At the end of each request, the User object is serialized to the session. -On the next request, it's unserialized. To help PHP do this correctly, you -need to implement ``Serializable``. But you don't need to serialize everything: -you only need a few fields (the ones shown above plus a few extra if you -decide to implement :ref:`AdvancedUserInterface `). -On each request, the ``id`` is used to query for a fresh ``User`` object -from the database. - -Want to know more? See :ref:`security-serialize-equatable`. - -.. _authenticating-someone-against-a-database: -.. _security-config-entity-provider: - -2) Configure Security to load from your Entity ----------------------------------------------- - -Now that you have a ``User`` entity that implements ``UserInterface``, you -just need to tell Symfony's security system about it in ``security.yml``. - -In this example, the user will enter their username and password via HTTP -basic authentication. Symfony will query for a ``User`` entity matching -the username and then check the password (more on passwords in a moment): - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - encoders: - AppBundle\Entity\User: - algorithm: bcrypt - - # ... - - providers: - our_db_provider: - entity: - class: AppBundle:User - property: username - # if you're using multiple entity managers - # manager_name: customer - - firewalls: - main: - pattern: ^/ - http_basic: ~ - provider: our_db_provider - - # ... - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - use AppBundle\Entity\User; - - $container->loadFromExtension('security', [ - 'encoders' => [ - User::class => [ - 'algorithm' => 'bcrypt', - ], - ], - - // ... - - 'providers' => [ - 'our_db_provider' => [ - 'entity' => [ - 'class' => 'AppBundle:User', - 'property' => 'username', - ], - ], - ], - 'firewalls' => [ - 'main' => [ - 'pattern' => '^/', - 'http_basic' => null, - 'provider' => 'our_db_provider', - ], - ], - - // ... - ]); - -First, the ``encoders`` section tells Symfony to expect that the passwords -in the database will be encoded using ``bcrypt``. Second, the ``providers`` -section creates a "user provider" called ``our_db_provider`` that knows to -query from your ``AppBundle:User`` entity by the ``username`` property. The -name ``our_db_provider`` isn't important: it just needs to match the value -of the ``provider`` key under your firewall. Or, if you don't set the ``provider`` -key under your firewall, the first "user provider" is automatically used. - -Creating your First User -~~~~~~~~~~~~~~~~~~~~~~~~ - -To add users, you can implement a :doc:`registration form ` -or add some `fixtures`_. This is just a normal entity, so there's nothing -tricky, *except* that you need to encode each user's password. But don't -worry, Symfony gives you a service that will do this for you. See :doc:`/security/password_encoding` -for details. - -Below is an export of the ``app_users`` table from MySQL with user ``admin`` -and password ``admin`` (which has been encoded). - -.. code-block:: terminal - - $ mysql> SELECT * FROM app_users; - +----+----------+--------------------------------------------------------------+--------------------+-----------+ - | id | username | password | email | is_active | - +----+----------+--------------------------------------------------------------+--------------------+-----------+ - | 1 | admin | $2a$08$jHZj/wJfcVKlIwr5AvR78euJxYK7Ku5kURNhNx.7.CSIJ3Pq6LEPC | admin@example.com | 1 | - +----+----------+--------------------------------------------------------------+--------------------+-----------+ - -.. sidebar:: Do you need to use a Salt property? - - If you use ``bcrypt`` or ``argon2i``, no. Otherwise, yes. All passwords must - be hashed with a salt, but ``bcrypt`` and ``argon2i`` do this internally. - Since this tutorial *does* use ``bcrypt``, the ``getSalt()`` method in - ``User`` can just return ``null`` (it's not used). If you use a different - algorithm, you'll need to uncomment the ``salt`` lines in the ``User`` - entity and add a persisted ``salt`` property. - -.. _security-advanced-user-interface: - -Forbid Inactive Users (AdvancedUserInterface) ---------------------------------------------- - -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 to the site -normally. - -To exclude inactive users, change your ``User`` class to implement -:class:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface`. -This extends :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`, -so you only need the new interface:: - - // src/AppBundle/Entity/User.php - 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; - } - - // serialize and unserialize must be updated - see below - public function serialize() - { - return serialize([ - // ... - $this->isActive, - ]); - } - public function unserialize($serialized) - { - list ( - // ... - $this->isActive, - ) = unserialize($serialized); - } - } - -The :class:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface` -interface adds four extra methods to validate the account status: - -* :method:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface::isAccountNonExpired` - checks whether the user's account has expired; -* :method:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface::isAccountNonLocked` - checks whether the user is locked; -* :method:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface::isCredentialsNonExpired` - checks whether the user's credentials (password) has expired; -* :method:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface::isEnabled` - checks whether the user is enabled. - -If *any* of these return ``false``, the user won't be allowed to login. You -can choose to have persisted properties for all of these, or whatever you -need (in this example, only ``isActive`` pulls from the database). - -So what's the difference between the methods? Each returns a slightly different -error message (and these can be translated when you render them in your login -template to customize them further). - -.. note:: - - If you use ``AdvancedUserInterface``, you also need to add any of the - properties used by these methods (like ``isActive``) to the ``serialize()`` - and ``unserialize()`` methods. If you *don't* do this, your user may - not be deserialized correctly from the session on each request. - -Congrats! Your database-loading security system is all setup! Next, add a -true :doc:`login form ` instead of HTTP Basic -or keep reading for other topics. - -.. _authenticating-someone-with-a-custom-entity-provider: - -Using a Custom Query to Load the User -------------------------------------- - -It would be great if a user could login with their username *or* email, as -both are unique in the database. Unfortunately, the native entity provider -is only able to handle querying via a single property on the user. - -To do this, make your ``UserRepository`` implement a special -:class:`Symfony\\Bridge\\Doctrine\\Security\\User\\UserLoaderInterface`. This -interface only requires one method: ``loadUserByUsername($username)``:: - - // src/AppBundle/Repository/UserRepository.php - namespace AppBundle\Repository; - - use Doctrine\ORM\EntityRepository; - use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface; - - class UserRepository extends EntityRepository implements UserLoaderInterface - { - public function loadUserByUsername($username) - { - return $this->createQueryBuilder('u') - ->where('u.username = :username OR u.email = :email') - ->setParameter('username', $username) - ->setParameter('email', $username) - ->getQuery() - ->getOneOrNullResult(); - } - } - -.. tip:: - - Don't forget to add the repository class to the - :doc:`mapping definition of your entity `. - -To finish this, just remove the ``property`` key from the user provider in -``security.yml``: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - providers: - our_db_provider: - entity: - class: AppBundle:User - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', [ - // ... - - 'providers' => [ - 'our_db_provider' => [ - 'entity' => [ - 'class' => 'AppBundle:User', - ], - ], - ], - ]); - -This tells Symfony to *not* query automatically for the User. Instead, when -someone logs in, the ``loadUserByUsername()`` method on ``UserRepository`` -will be called. - -.. _security-serialize-equatable: - -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, the 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 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 :phpclass:`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 :method:`Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider::refreshUser` -method refreshes the user on each request by using the ``id`` (as explained -above). This gives us a "fresh" User object. - -But Symfony also uses the ``username``, ``salt``, and ``password`` to verify -that the User has not changed between requests (it also calls your ``AdvancedUserInterface`` -methods if you implement it). Failing to serialize these may cause you to -be logged out on each request. If your user implements the -:class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface`, -then instead of these properties being checked, your :method:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface::isEqualTo` method -is 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. - -.. _fixtures: https://symfony.com/doc/master/bundles/DoctrineFixturesBundle/index.html -.. _FOSUserBundle: https://github.com/FriendsOfSymfony/FOSUserBundle -.. _`it must be quoted with backticks`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#quoting-reserved-words diff --git a/security/expressions.rst b/security/expressions.rst index 87aa4388ab6..2013d6656d7 100644 --- a/security/expressions.rst +++ b/security/expressions.rst @@ -15,7 +15,7 @@ accepts an :class:`Symfony\\Component\\ExpressionLanguage\\Expression` object:: use Symfony\Component\ExpressionLanguage\Expression; // ... - public function indexAction() + public function index() { $this->denyAccessUnlessGranted(new Expression( '"ROLE_ADMIN" in roles or (not is_anonymous() and user.isSuperAdmin())' @@ -63,26 +63,31 @@ Additionally, you have access to a number of functions inside the expression: Similar, but not equal to ``IS_AUTHENTICATED_REMEMBERED``, see below. ``is_fully_authenticated()`` Equal to checking if the user has the ``IS_AUTHENTICATED_FULLY`` role. -``has_role()`` - Checks to see if the user has the given role - equivalent to an expression like - ``'ROLE_ADMIN' in roles``. +``is_granted()`` + Checks if the user has the given permission. Optionally accepts a second argument + with the object where permission is checked on. It's equivalent to using + the :doc:`isGranted() method ` from the authorization + checker service. .. sidebar:: ``is_remember_me()`` is different than checking ``IS_AUTHENTICATED_REMEMBERED`` The ``is_remember_me()`` and ``is_fully_authenticated()`` functions are *similar* to using ``IS_AUTHENTICATED_REMEMBERED`` and ``IS_AUTHENTICATED_FULLY`` with the ``isGranted()`` function - but they are **not** the same. The - following shows the difference:: + following controller snippet shows the difference:: use Symfony\Component\ExpressionLanguage\Expression; + use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; // ... - $authorizationChecker = $this->get('security.authorization_checker'); - $access1 = $authorizationChecker->isGranted('IS_AUTHENTICATED_REMEMBERED'); + public function index(AuthorizationCheckerInterface $authorizationChecker) + { + $access1 = $authorizationChecker->isGranted('IS_AUTHENTICATED_REMEMBERED'); - $access2 = $authorizationChecker->isGranted(new Expression( - 'is_remember_me() or is_fully_authenticated()' - )); + $access2 = $authorizationChecker->isGranted(new Expression( + 'is_remember_me() or is_fully_authenticated()' + )); + } Here, ``$access1`` and ``$access2`` will be the same value. Unlike the behavior of ``IS_AUTHENTICATED_REMEMBERED`` and ``IS_AUTHENTICATED_FULLY``, diff --git a/security/firewall_restriction.rst b/security/firewall_restriction.rst index c41a66e4e22..0ce0c2e3ff5 100644 --- a/security/firewall_restriction.rst +++ b/security/firewall_restriction.rst @@ -1,8 +1,8 @@ .. index:: single: Security; Restrict Security Firewalls to a Request -How to Restrict Firewalls to a Specific Request -=============================================== +How to Restrict Firewalls to a Request +====================================== When using the Security component, firewalls will decide whether they handle a request based on the result of a request matcher: the first firewall matching @@ -32,7 +32,7 @@ if the request path matches the configured ``pattern``. .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml # ... security: @@ -43,7 +43,7 @@ if the request path matches the configured ``pattern``. .. code-block:: xml - + loadFromExtension('security', [ @@ -89,7 +89,7 @@ only initialize if the host from the request matches against the configuration. .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml # ... security: @@ -100,7 +100,7 @@ only initialize if the host from the request matches against the configuration. .. code-block:: xml - + loadFromExtension('security', [ @@ -147,7 +147,7 @@ the provided HTTP methods. .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml # ... security: @@ -158,7 +158,7 @@ the provided HTTP methods. .. code-block:: xml - + loadFromExtension('security', [ @@ -203,7 +203,7 @@ If the above options don't fit your needs you can configure any service implemen .. code-block:: yaml - # app/config/security.yaml + # config/packages/security.yaml # ... security: @@ -214,7 +214,7 @@ If the above options don't fit your needs you can configure any service implemen .. code-block:: xml - + loadFromExtension('security', [ diff --git a/security/force_https.rst b/security/force_https.rst index 65d5138ed56..98ad24176d7 100644 --- a/security/force_https.rst +++ b/security/force_https.rst @@ -4,25 +4,33 @@ How to Force HTTPS or HTTP for different URLs ============================================= +.. tip:: + + The *best* policy is to force ``https`` on all URLs, which can be done via + your web server configuration or ``access_control``. + 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: +option. To enforce HTTPS on all URLs, add the ``requires_channel`` config to every +access control: .. configuration-block:: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... access_control: - { path: '^/secure', roles: ROLE_ADMIN, requires_channel: https } + - { path: '^/login', roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } + # catch all other URLs + - { path: '^/', roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } .. code-block:: xml - + + +
.. code-block:: php - // app/config/security.php + // config/packages/security.php $container->loadFromExtension('security', [ // ... @@ -49,62 +65,30 @@ to use HTTPS then you could use the following configuration: 'roles' => 'ROLE_ADMIN', 'requires_channel' => 'https', ], + [ + 'path' => '^/login', + 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', + 'requires_channel' => 'https', + ], + [ + 'path' => '^/', + 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', + '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: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - access_control: - - { path: '^/login', roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } - - .. code-block:: xml - - - - +To make life easier while developing, you can also use an environment variable, +like ``requires_channel: '%env(SECURE_SCHEME)%'``. In your ``.env`` file, set +``SECURE_SCHEME`` to ``http`` by default, but override it to ``https`` on production. - - +See :doc:`/security/access_control` for more details about ``access_control`` +in general. - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', [ - // ... - - 'access_control' => [ - [ - 'path' => '^/login', - 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY', - 'requires_channel' => 'https', - ], - ], - ]); +.. note:: -It is also possible to specify using HTTPS in the routing configuration, -see :doc:`/routing/scheme` for more details. + An alternative way to enforce HTTP or HTTPS is to use + :ref:`the scheme option ` of a route or group of routes. .. note:: diff --git a/security/form_login.rst b/security/form_login.rst index d4f676da939..2d2775b8f25 100644 --- a/security/form_login.rst +++ b/security/form_login.rst @@ -1,15 +1,397 @@ .. index:: single: Security; Customizing form login redirect -How to Customize Redirect After Form Login -========================================== +Using the form_login Authentication Provider +============================================ -Using a :doc:`form login ` for authentication is a -common, and flexible, method for handling authentication in Symfony. This -article explains how to customize the URL which the user is redirected to after -a successful or failed login. Check out the full -:doc:`form login configuration reference ` to -learn of the possible customization options. +.. caution:: + + To have complete control over your login form, we recommend building a + :doc:`form login authentication with Guard `. + +Symfony comes with a built-in ``form_login`` system that handles a login form +POST automatically. Before you start, make sure you've followed the +:doc:`Security Guide ` to create your User class. + +form_login Setup +---------------- + +First, enable ``form_login`` under your firewall: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + anonymous: ~ + form_login: + login_path: login + check_path: login + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + 'firewalls' => [ + 'main' => [ + 'anonymous' => null, + 'form_login' => [ + 'login_path' => 'login', + 'check_path' => 'login', + ], + ], + ], + ]); + +.. tip:: + + 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 +is your job. First, create a new ``SecurityController``:: + + // src/Controller/SecurityController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + + class SecurityController extends AbstractController + { + } + +Next, configure the route that you earlier used under your ``form_login`` +configuration (``login``): + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Controller/SecurityController.php + + // ... + use Symfony\Component\Routing\Annotation\Route; + + class SecurityController extends AbstractController + { + /** + * @Route("/login", name="login", methods={"GET", "POST"}) + */ + public function login() + { + } + } + + .. code-block:: yaml + + # config/routes.yaml + login: + path: /login + controller: App\Controller\SecurityController::login + methods: GET|POST + + .. code-block:: xml + + + + + + + + + .. code-block:: php + + // config/routes.php + use App\Controller\SecurityController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + $routes->add('login', '/login') + ->controller([SecurityController::class, 'login']) + ->methods(['GET', 'POST']) + ; + }; + +Great! Next, add the logic to ``login()`` that displays the login form:: + + // src/Controller/SecurityController.php + use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; + + public function login(AuthenticationUtils $authenticationUtils) + { + // get the login error if there is one + $error = $authenticationUtils->getLastAuthenticationError(); + + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/login.html.twig', [ + 'last_username' => $lastUsername, + 'error' => $error, + ]); + } + +.. note:: + + If you get an error that the ``$authenticationUtils`` argument is missing, + it's probably because the controllers of your application are not defined as + services and tagged with the ``controller.service_arguments`` tag, as done + in the :ref:`default services.yaml configuration `. + +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 submits 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: + +.. code-block:: html+twig + + {# templates/security/login.html.twig #} + {# ... you will probably extend your base template, like base.html.twig #} + + {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} + +
+ + + + + + + {# + If you want to control the URL the user + is redirected to on success (more details below) + + #} + + +
+ +.. tip:: + + The ``error`` variable passed into the template is an instance of + :class:`Symfony\\Component\\Security\\Core\\Exception\\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 it usually follows some conventions: + +* The ``
`` element sends a ``POST`` request to the ``login`` route, since + that's what you configured under the ``form_login`` key in ``security.yaml``; +* The username field has the name ``_username`` and the password field has the + name ``_password``. + +.. tip:: + + Actually, all of this can be configured under the ``form_login`` key. See + :ref:`reference-security-firewall-form-login` for more details. + +.. caution:: + + This login form is currently not protected against CSRF attacks. Read + :ref:`form_login-csrf` 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``; +#. 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. + +.. _form_login-csrf: + +CSRF Protection in Login Forms +------------------------------ + +`Login CSRF attacks`_ can be prevented using the same technique of adding hidden +CSRF tokens into the login forms. The Security component already provides CSRF +protection, but you need to configure some options before using it. + +First, configure the CSRF token provider used by the form login in your security +configuration. You can set this to use the default provider available in the +security component: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + secured_area: + # ... + form_login: + # ... + csrf_token_generator: security.csrf.token_manager + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + // ... + + 'firewalls' => [ + 'secured_area' => [ + // ... + 'form_login' => [ + // ... + 'csrf_token_generator' => 'security.csrf.token_manager', + ], + ], + ], + ]); + +.. _csrf-login-template: + +Then, use the ``csrf_token()`` function in the Twig template to generate a CSRF +token and store it as a hidden field of the form. By default, the HTML field +must be called ``_csrf_token`` and the string used to generate the value must +be ``authenticate``: + +.. code-block:: html+twig + + {# templates/security/login.html.twig #} + + {# ... #} + + {# ... the login fields #} + + + + +
+ +After this, you have protected your login form against CSRF attacks. + +.. tip:: + + You can change the name of the field by setting ``csrf_parameter`` and change + the token ID by setting ``csrf_token_id`` in your configuration: + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + secured_area: + # ... + form_login: + # ... + csrf_parameter: _csrf_security_token + csrf_token_id: a_private_string + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + // ... + + 'firewalls' => [ + 'secured_area' => [ + // ... + 'form_login' => [ + // ... + 'csrf_parameter' => '_csrf_security_token', + 'csrf_token_id' => 'a_private_string', + ], + ], + ], + ]); Redirecting after Success ------------------------- @@ -35,7 +417,7 @@ a relative/absolute URL or a Symfony route name: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -47,7 +429,7 @@ a relative/absolute URL or a Symfony route name: .. code-block:: xml - + loadFromExtension('security', [ // ... @@ -92,7 +474,7 @@ previously requested URL and always redirect to the default page: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -104,7 +486,7 @@ previously requested URL and always redirect to the default page: .. code-block:: xml - + loadFromExtension('security', [ // ... @@ -159,7 +541,7 @@ Defining the redirect URL via POST using a hidden form field: .. code-block:: html+twig - {# app/Resources/views/security/login.html.twig #} + {# templates/security/login.html.twig #}
{# ... #} @@ -179,7 +561,7 @@ parameter is included in the request, you may use the value of the .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -192,7 +574,7 @@ parameter is included in the request, you may use the value of the .. code-block:: xml - + loadFromExtension('security', [ // ... @@ -245,7 +627,7 @@ option to define a new target via a relative/absolute URL or a Symfony route nam .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -258,7 +640,7 @@ option to define a new target via a relative/absolute URL or a Symfony route nam .. code-block:: xml - + loadFromExtension('security', [ // ... @@ -301,7 +683,7 @@ This option can also be set via the ``_failure_path`` request parameter: .. code-block:: html+twig - {# app/Resources/views/security/login.html.twig #} + {# templates/security/login.html.twig #} {# ... #} @@ -320,7 +702,7 @@ redirects can be customized using the ``target_path_parameter`` and .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -333,7 +715,7 @@ redirects can be customized using the ``target_path_parameter`` and .. code-block:: xml - + loadFromExtension('security', [ // ... @@ -378,7 +760,7 @@ are now fully customized: .. code-block:: html+twig - {# app/Resources/views/security/login.html.twig #} + {# templates/security/login.html.twig #} {# ... #} @@ -387,24 +769,4 @@ are now fully customized: -Redirecting to the Last Accessed Page with ``TargetPathTrait`` --------------------------------------------------------------- - -The last request URI is stored in a session variable named -``_security..target_path`` (e.g. ``_security.main.target_path`` -if the name of your firewall is ``main``). Most of the times you don't have to -deal with this low level session variable. However, if you ever need to get or -remove this variable, it's better to use the -:class:`Symfony\\Component\\Security\\Http\\Util\\TargetPathTrait` utility:: - - // ... - use Symfony\Component\Security\Http\Util\TargetPathTrait; - - $targetPath = $this->getTargetPath($request->getSession(), $providerKey); - - // equivalent to: - // $targetPath = $request->getSession()->get('_security.'.$providerKey.'.target_path'); - -.. versionadded:: 3.1 - - The ``TargetPathTrait`` was introduced in Symfony 3.1. +.. _`Login CSRF attacks`: https://en.wikipedia.org/wiki/Cross-site_request_forgery#Forging_login_requests diff --git a/security/form_login_setup.rst b/security/form_login_setup.rst index 330f9476701..dafbc783dca 100644 --- a/security/form_login_setup.rst +++ b/security/form_login_setup.rst @@ -1,381 +1,471 @@ -How to Build a Traditional Login Form -===================================== +How to Build a Login Form +========================= -.. tip:: +.. seealso:: - 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. + If you're looking for the ``form_login`` firewall option, see + :doc:`/security/form_login`. -In this entry, you'll build a traditional login form. When the -user logs in, you can load your users from anywhere - like the database. -See :ref:`security-user-providers` for details. +Ready to create a login form? First, make sure you've followed the main +:doc:`Security Guide ` to install security and create your ``User`` +class. -First, enable form login under your firewall: +Generating the Login Form +------------------------- + +Creating a powerful login form can be bootstrapped with the ``make:auth`` command from +`MakerBundle`_. Depending on your setup, you may be asked different questions +and your generated code may be slightly different: + +.. code-block:: terminal + + $ php bin/console make:auth + + What style of authentication do you want? [Empty authenticator]: + [0] Empty authenticator + [1] Login form authenticator + > 1 + + The class name of the authenticator to create (e.g. AppCustomAuthenticator): + > LoginFormAuthenticator + + Choose a name for the controller class (e.g. SecurityController) [SecurityController]: + > SecurityController + + Do you want to generate a '/logout' URL? (yes/no) [yes]: + > yes + + created: src/Security/LoginFormAuthenticator.php + updated: config/packages/security.yaml + created: src/Controller/SecurityController.php + created: templates/security/login.html.twig + +.. versionadded:: 1.8 + + Support for login form authentication was added to ``make:auth`` in MakerBundle 1.8. + +This generates the following: 1) a login route & controller, 2) a template that +renders the login form, 3) a :doc:`Guard authenticator ` +class that processes the login submit and 4) updates the main security config file. + +**Step 1.** The ``/login`` route & controller:: + + // src/Controller/SecurityController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; + + class SecurityController extends AbstractController + { + /** + * @Route("/login", name="app_login") + */ + public function login(AuthenticationUtils $authenticationUtils): Response + { + // get the login error if there is one + $error = $authenticationUtils->getLastAuthenticationError(); + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/login.html.twig', [ + 'last_username' => $lastUsername, + 'error' => $error + ]); + } + } + +Edit the ``security.yaml`` file in order to allow access for anyone to the +``/login`` route: .. configuration-block:: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... - firewalls: - main: - anonymous: ~ - form_login: - login_path: login - check_path: login + access_control: + - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY } + # ... .. code-block:: xml - - + + - - - - + + .. code-block:: php - // app/config/security.php + // config/packages/security.php $container->loadFromExtension('security', [ - 'firewalls' => [ - 'main' => [ - 'anonymous' => null, - 'form_login' => [ - 'login_path' => 'login', - 'check_path' => 'login', - ], + // ... + 'access_control' => [ + [ + 'path' => '^/login', + 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY', ], + // ... ], ]); -.. tip:: - - 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). +**Step 2.** The template has very little to do with security: it just generates +a traditional HTML form that submits to ``/login``: -Now, when the security system initiates the authentication process, it will -redirect the user to the login form ``/login``. Implementing this login form -is your job. First, create a new ``SecurityController`` inside a bundle:: +.. code-block:: html+twig - // src/AppBundle/Controller/SecurityController.php - namespace AppBundle\Controller; + {% extends 'base.html.twig' %} - use Symfony\Bundle\FrameworkBundle\Controller\Controller; + {% block title %}Log in!{% endblock %} - class SecurityController extends Controller - { - } + {% block body %} +
+ {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} -Next, configure the route that you earlier used under your ``form_login`` -configuration (``login``): +

Please sign in

+ + + + -.. configuration-block:: + - .. code-block:: php-annotations + {# + Uncomment this section and add a remember_me option below your firewall to activate remember me functionality. + See https://symfony.com/doc/current/security/remember_me.html + +
+ +
+ #} - // src/AppBundle/Controller/SecurityController.php + +
+ {% endblock %} + +**Step 3.** The Guard authenticator processes the form submit:: + + // src/Security/LoginFormAuthenticator.php + namespace App\Security; + + use App\Entity\User; + use Doctrine\ORM\EntityManagerInterface; + + use Symfony\Component\HttpFoundation\RedirectResponse; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Routing\RouterInterface; + use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; + use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; + use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; + use Symfony\Component\Security\Core\Security; + use Symfony\Component\Security\Core\User\UserInterface; + use Symfony\Component\Security\Core\User\UserProviderInterface; + use Symfony\Component\Security\Csrf\CsrfToken; + use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; + use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; + use Symfony\Component\Security\Http\Util\TargetPathTrait; + + class LoginFormAuthenticator extends AbstractFormLoginAuthenticator + { + use TargetPathTrait; - // ... - use Symfony\Component\Routing\Annotation\Route; + private $entityManager; + private $router; + private $csrfTokenManager; + private $passwordEncoder; - class SecurityController extends Controller + public function __construct(EntityManagerInterface $entityManager, RouterInterface $router, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder) { - /** - * @Route("/login", name="login") - */ - public function loginAction() - { - } + $this->entityManager = $entityManager; + $this->router = $router; + $this->csrfTokenManager = $csrfTokenManager; + $this->passwordEncoder = $passwordEncoder; } - .. code-block:: yaml - - # app/config/routing.yml - login: - path: /login - defaults: { _controller: AppBundle:Security:login } - - .. code-block:: xml - - - - - - - AppBundle:Security:login - - + public function supports(Request $request) + { + return 'app_login' === $request->attributes->get('_route') + && $request->isMethod('POST'); + } - .. code-block:: php + public function getCredentials(Request $request) + { + $credentials = [ + 'email' => $request->request->get('email'), + 'password' => $request->request->get('password'), + 'csrf_token' => $request->request->get('_csrf_token'), + ]; + $request->getSession()->set( + Security::LAST_USERNAME, + $credentials['email'] + ); + + return $credentials; + } - // app/config/routing.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; + public function getUser($credentials, UserProviderInterface $userProvider) + { + $token = new CsrfToken('authenticate', $credentials['csrf_token']); + if (!$this->csrfTokenManager->isTokenValid($token)) { + throw new InvalidCsrfTokenException(); + } - $routes = new RouteCollection(); - $routes->add('login', new Route('/login', [ - '_controller' => 'AppBundle:Security:login', - ])); + $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]); - return $routes; + if (!$user) { + // fail authentication with a custom error + throw new CustomUserMessageAuthenticationException('Email could not be found.'); + } -Great! Next, add the logic to ``loginAction()`` that displays the login form:: + return $user; + } - // src/AppBundle/Controller/SecurityController.php - use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; + public function checkCredentials($credentials, UserInterface $user) + { + return $this->passwordEncoder->isPasswordValid($user, $credentials['password']); + } - public function loginAction(AuthenticationUtils $authenticationUtils) - { - // get the login error if there is one - $error = $authenticationUtils->getLastAuthenticationError(); + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) { + return new RedirectResponse($targetPath); + } - // last username entered by the user - $lastUsername = $authenticationUtils->getLastUsername(); + // For example : return new RedirectResponse($this->router->generate('some_route')); + throw new \Exception('TODO: provide a valid redirect inside '.__FILE__); + } - return $this->render('security/login.html.twig', [ - 'last_username' => $lastUsername, - 'error' => $error, - ]); + protected function getLoginUrl() + { + return $this->router->generate('app_login'); + } } -.. note:: - - If you get an error that the ``$authenticationUtils`` argument is missing, - it's probably because you need to activate this new feature in Symfony 3.4. - See this :ref:`controller service argument note `. +**Step 4.** Updates the main security config file to enable the Guard authenticator: -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 submits 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: - -.. code-block:: html+twig +.. configuration-block:: - {# app/Resources/views/security/login.html.twig #} - {# ... you will probably extend your base template, like base.html.twig #} + .. code-block:: yaml - {% if error %} -
{{ error.messageKey|trans(error.messageData, 'security') }}
- {% endif %} + # config/packages/security.yaml + security: + # ... -
- - + firewalls: + main: + # ... + guard: + authenticators: + - App\Security\LoginFormAuthenticator - - + .. code-block:: xml - {# - If you want to control the URL the user - is redirected to on success (more details below) - - #} + + + - - + + + + + + + + + +
-.. tip:: + .. code-block:: php - The ``error`` variable passed into the template is an instance of - :class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException`. - It may contain more information - or even sensitive information - about - the authentication failure, so use it wisely! + // config/packages/security.php + use App\Security\LoginFormAuthenticator; -The form can look like anything, but it usually follows some conventions: + $container->loadFromExtension('security', [ + // ... + 'firewalls' => [ + 'main' => [ + // ..., + 'guard' => [ + 'authenticators' => [ + LoginFormAuthenticator::class, + ] + ], + ], + ], + ]); -* The ``
`` element sends a ``POST`` request to the ``login`` route, since - that's what you configured under the ``form_login`` key in ``security.yml``; -* The username field has the name ``_username`` and the password field has the - name ``_password``. +Finishing the Login Form +------------------------ -.. tip:: +Woh. The ``make:auth`` command just did a *lot* of work for you. But, you're not done +yet. First, go to ``/login`` to see the new login form. Feel free to customize this +however you want. - Actually, all of this can be configured under the ``form_login`` key. See - :ref:`reference-security-firewall-form-login` for more details. +When you submit the form, the ``LoginFormAuthenticator`` will intercept the request, +read the email (or whatever field you're using) & password from the form, find the +``User`` object, validate the CSRF token and check the password. -.. caution:: +But, depending on your setup, you'll need to finish one or more TODOs before the +whole process works. You will *at least* need to fill in *where* you want your user to +be redirected after success: - This login form is currently not protected against CSRF attacks. Read - :doc:`/security/csrf_in_login_form` on how to protect your login - form. +.. code-block:: diff -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. + // src/Security/LoginFormAuthenticator.php -To review the whole process: + // ... + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + // ... -#. 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``; -#. 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. + - throw new \Exception('TODO: provide a valid redirect inside '.__FILE__); + + // redirect to some "app_homepage" route - of wherever you want + + return new RedirectResponse($this->urlGenerator->generate('app_homepage')); + } -Redirecting after Success -------------------------- +Unless you have any other TODOs in that file, that's it! If you're loading users +from the database, make sure you've loaded some :ref:`dummy users `. +Then, try to login. -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. +If you're successful, the web debug toolbar will tell you who you are and what roles +you have: -For more details on this and how to customize the form login process in general, -see :doc:`/security/form_login`. +.. image:: /_images/security/symfony_loggedin_wdt.png + :align: center -.. _security-common-pitfalls: +The Guard authentication system is powerful, and you can customize your authenticator +class to do whatever you need. To learn more about what the individual methods do, +see :doc:`/security/guard_authentication`. -Avoid Common Pitfalls ---------------------- +Controlling Error Messages +-------------------------- -When setting up your login form, watch out for a few common pitfalls. +You can cause authentication to fail with a custom message at any step by throwing +a custom :class:`Symfony\\Component\\Security\\Core\\Exception\\CustomUserMessageAuthenticationException`. +This is an easy way to control the error message. -1. Create the Correct Routes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +But in some cases, like if you return ``false`` from ``checkCredentials()``, you +may see an error that comes from the core of Symfony - like ``Invalid credentials.``. -First, be sure that you've defined the ``/login`` route correctly and that -it corresponds 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). +To customize this message, you could throw a ``CustomUserMessageAuthenticationException`` +instead. Or, you can :doc:`translate ` the message through the ``security`` +domain: -2. Be Sure the Login Page Isn't Secure (Redirect Loop!) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. configuration-block:: -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: + .. code-block:: xml -.. configuration-block:: + + + + + + + Invalid credentials. + The password you entered was invalid! + + + + .. code-block:: yaml - # app/config/security.yml + # translations/security.en.yaml + 'Invalid credentials.': 'The password you entered was invalid!' - # ... - access_control: - - { path: '^/', roles: ROLE_ADMIN } + .. code-block:: php - .. code-block:: xml + // translations/security.en.php + return [ + 'Invalid credentials.' => 'The password you entered was invalid!', + ]; - - - +If the message isn't translated, make sure you've installed the ``translator`` +and try clearing your cache: - - - - - +.. code-block:: terminal - .. code-block:: php + $ php bin/console cache:clear - // app/config/security.php +Redirecting to the Last Accessed Page with ``TargetPathTrait`` +-------------------------------------------------------------- - // ... - 'access_control' => [ - ['path' => '^/', 'roles' => 'ROLE_ADMIN'], - ], +The last request URI is stored in a session variable named +``_security..target_path`` (e.g. ``_security.main.target_path`` +if the name of your firewall is ``main``). Most of the times you don't have to +deal with this low level session variable. However, the +:class:`Symfony\\Component\\Security\\Http\\Util\\TargetPathTrait` utility +can be used to read (like in the example above) or set this value manually. -Adding an access control that matches ``/login/*`` and requires *no* authentication -fixes the problem: +When the user tries to access a restricted page, they are being redirected to +the login page. At that point target path will be set. After a successful login, +the user will be redirected to this previously set target path. -.. configuration-block:: +If you also want to apply this behavior to public pages, you can create an +:doc:`event subscriber ` to set the target path manually +whenever the user browses a page:: - .. code-block:: yaml + namespace App\EventSubscriber; - # app/config/security.yml + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpFoundation\Session\SessionInterface; + use Symfony\Component\HttpKernel\Event\RequestEvent; + use Symfony\Component\HttpKernel\KernelEvents; + use Symfony\Component\Security\Http\Util\TargetPathTrait; - # ... - access_control: - - { path: '^/login', roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: '^/', roles: ROLE_ADMIN } + class RequestSubscriber implements EventSubscriberInterface + { + use TargetPathTrait; - .. code-block:: xml + private $session; - - - + public function __construct(SessionInterface $session) + { + $this->session = $session; + } - - - - - - + public function onKernelRequest(RequestEvent $event): void + { + $request = $event->getRequest(); + if (!$event->isMasterRequest() || $request->isXmlHttpRequest()) { + return; + } - .. code-block:: php + $this->saveTargetPath($this->session, 'main', $request->getUri()); + } - // app/config/security.php + public static function getSubscribedEvents() + { + return [ + KernelEvents::REQUEST => ['onKernelRequest'] + ]; + } + } - // ... - 'access_control' => [ - ['path' => '^/login', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'], - ['path' => '^/', 'roles' => 'ROLE_ADMIN'], - ], - -3. Be Sure check_path Is Behind a Firewall -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Next, make sure that your ``check_path`` URL (e.g. ``/login``) is behind -the firewall you're using for your form login (in this example, the single -firewall matches *all* URLs, including ``/login``). If ``/login`` -doesn't match any firewall, you'll receive a ``Unable to find the controller -for path "/login"`` exception. - -4. Multiple Firewalls Don't Share the Same 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 :ref:`reference-security-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 :doc:`/controller/error_pages` -for more details. - -.. _`FOSUserBundle`: https://github.com/FriendsOfSymfony/FOSUserBundle +.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html diff --git a/security/guard_authentication.rst b/security/guard_authentication.rst index 14159a25cc0..13d1b5486d8 100644 --- a/security/guard_authentication.rst +++ b/security/guard_authentication.rst @@ -1,165 +1,71 @@ .. index:: single: Security; Custom Authentication -How to Create a Custom Authentication System with Guard -======================================================= +Custom Authentication System with Guard (API Token Example) +=========================================================== Whether you need to build a traditional login form, an API token authentication system or you need to integrate with some proprietary single-sign-on system, the Guard -component can make it easy... and fun! +component will be the right choice! -In this example, you'll build an API token authentication system and learn how -to work with Guard. +Guard authentication can be used to: -Create a User and a User Provider ---------------------------------- +* :doc:`Build a Login Form `, +* Create an API token authentication system (done on this page!) +* `Social Authentication`_ (or use `HWIOAuthBundle`_ for a robust, but non-Guard solution) -No matter how you authenticate, you need to create a User class that implements ``UserInterface`` -and configure a :doc:`user provider `. In this -example, users are stored in the database via Doctrine, and each user has an ``apiKey`` -property they use to access their account via the API:: +or anything else. In this example, we'll build an API token authentication +system so we can learn more about Guard in detail. - // src/AppBundle/Entity/User.php - namespace AppBundle\Entity; - - use Doctrine\ORM\Mapping as ORM; - use Symfony\Component\Security\Core\User\UserInterface; +Step 1) Prepare your User Class +------------------------------- - /** - * @ORM\Entity - * @ORM\Table(name="`user`") - */ - class User implements UserInterface - { - /** - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - * @ORM\Column(type="integer") - */ - private $id; +Suppose you want to build an API where your clients will send an ``X-AUTH-TOKEN`` header +on each request with their API token. Your job is to read this and find the associated +user (if any). - /** - * @ORM\Column(type="string", unique=true) - */ - private $username; +First, make sure you've followed the main :doc:`Security Guide ` to +create your ``User`` class. Then, to keep things simple, add an ``apiToken`` property +directly to your ``User`` class (the ``make:entity`` command is a good way to do this): - /** - * @ORM\Column(type="string", unique=true) - */ - private $apiKey; +.. code-block:: diff - public function getUsername() - { - return $this->username; - } + // src/Entity/User.php + // ... - public function getRoles() - { - return ['ROLE_USER']; - } + class User implements UserInterface + { + // ... - public function getPassword() - { - } - public function getSalt() - { - } - public function eraseCredentials() - { - } + + /** + + * @ORM\Column(type="string", unique=true, nullable=true) + + */ + + private $apiToken; - // more getters/setters + // the getter and setter methods } -.. caution:: - - In the example above, the table name is ``user``. This is a reserved SQL - keyword and `must be quoted with backticks`_ in Doctrine to avoid errors. - You might also change the table name (e.g. with ``app_users``) to solve - this issue. - -.. tip:: - - This User doesn't have a password, but you can add a ``password`` property if - you also want to allow this user to login with a password (e.g. via a login form). - -Your ``User`` class doesn't need to be stored in Doctrine: do whatever you need. -Next, make sure you've configured a "user provider" for the user: +Don't forget to generate and execute the migration: -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - providers: - your_db_provider: - entity: - class: AppBundle:User - property: apiKey - - # ... - - .. code-block:: xml - - - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', [ - // ... - - 'providers' => [ - 'your_db_provider' => [ - 'entity' => [ - 'class' => 'AppBundle:User', - ], - ], - ], - - // ... - ]); - -That's it! Need more information about this step, see: +.. code-block:: terminal -* :doc:`/security/entity_provider` -* :doc:`/security/custom_provider` + $ php bin/console make:migration + $ php bin/console doctrine:migrations:migrate -Step 1) Create the Authenticator Class +Step 2) Create the Authenticator Class -------------------------------------- -Suppose you have an API where your clients will send an ``X-AUTH-TOKEN`` header -on each request with their API token. Your job is to read this and find the associated -user (if any). - -To create a custom authentication system, just create a class and make it implement +To create a custom authentication system, create a class and make it implement :class:`Symfony\\Component\\Security\\Guard\\AuthenticatorInterface`. Or, extend the simpler :class:`Symfony\\Component\\Security\\Guard\\AbstractGuardAuthenticator`. + This requires you to implement several methods:: - // src/AppBundle/Security/TokenAuthenticator.php - namespace AppBundle\Security; + // src/Security/TokenAuthenticator.php + namespace App\Security; + use App\Entity\User; + use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -171,6 +77,13 @@ This requires you to implement several methods:: class TokenAuthenticator extends AbstractGuardAuthenticator { + private $em; + + public function __construct(EntityManagerInterface $em) + { + $this->em = $em; + } + /** * Called on every request to decide if this authenticator should be * used for the request. Returning false will cause this authenticator @@ -194,14 +107,15 @@ This requires you to implement several methods:: public function getUser($credentials, UserProviderInterface $userProvider) { - $apiKey = $credentials['token']; + $apiToken = $credentials['token']; - if (null === $apiKey) { + if (null === $apiToken) { return; } // if a User object, checkCredentials() is called - return $userProvider->loadUserByUsername($apiKey); + return $this->em->getRepository(User::class) + ->findOneBy(['apiToken' => $apiToken]); } public function checkCredentials($credentials, UserInterface $user) @@ -250,27 +164,22 @@ This requires you to implement several methods:: } } -.. versionadded:: 3.4 - - ``AuthenticatorInterface`` was introduced in Symfony 3.4. In previous Symfony - versions, authenticators needed to implement ``GuardAuthenticatorInterface``. - Nice work! Each method is explained below: :ref:`The Guard Authenticator Methods`. -Step 2) Configure the Authenticator +Step 3) Configure the Authenticator ----------------------------------- To finish this, make sure your authenticator is registered as a service. If you're -using the :ref:`default services.yml configuration `, +using the :ref:`default services.yaml configuration `, that happens automatically. -Finally, configure your ``firewalls`` key in ``security.yml`` to use this authenticator: +Finally, configure your ``firewalls`` key in ``security.yaml`` to use this authenticator: .. configuration-block:: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -283,17 +192,16 @@ Finally, configure your ``firewalls`` key in ``security.yml`` to use this authen guard: authenticators: - - AppBundle\Security\TokenAuthenticator + - App\Security\TokenAuthenticator # if you want, disable storing the user in the session # stateless: true - # maybe other things, like form_login, remember_me, etc # ... .. code-block:: xml - + - + - AppBundle\Security\TokenAuthenticator + App\Security\TokenAuthenticator @@ -320,10 +228,10 @@ Finally, configure your ``firewalls`` key in ``security.yml`` to use this authen .. code-block:: php - // app/config/security.php + // config/packages/security.php // ... - use AppBundle\Security\TokenAuthenticator; + use App\Security\TokenAuthenticator; $container->loadFromExtension('security', [ 'firewalls' => [ @@ -372,12 +280,6 @@ Each authenticator needs the following methods: authenticator should be used for this request (return ``true``) or if it should be skipped (return ``false``). - .. versionadded:: 3.4 - - The ``supports()`` method was introduced in Symfony 3.4. In previous Symfony - versions, the authenticator could be skipped returning ``null`` in the - ``getCredentials()`` method. - **getCredentials(Request $request)** This will be called on *every* request and your job is to read the token (or whatever your "authentication" information is) from the request and return it. @@ -394,11 +296,11 @@ Each authenticator needs the following methods: If ``getUser()`` returns a User object, this method is called. Your job is to verify if the credentials are correct. For a login form, this is where you would check that the password is correct for the user. To pass authentication, return - ``true``. If you return *anything* else + ``true``. If you return ``false`` (or throw an :ref:`AuthenticationException `), authentication will fail. -**onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)** +**onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)** This is called after successful authentication and your job is to either return a :class:`Symfony\\Component\\HttpFoundation\\Response` object that will be sent to the client or ``null`` to continue the request @@ -448,12 +350,12 @@ that describes *how* authentication failed via its ``$exception->getMessageKey() ``$exception->getMessageData()``) method. The message will be different based on *where* authentication fails (i.e. ``getUser()`` versus ``checkCredentials()``). -But, you can return a custom message by throwing a +But, you can also return a custom message by throwing a :class:`Symfony\\Component\\Security\\Core\\Exception\\CustomUserMessageAuthenticationException`. You can throw this from ``getCredentials()``, ``getUser()`` or ``checkCredentials()`` to cause a failure:: - // src/AppBundle/Security/TokenAuthenticator.php + // src/Security/TokenAuthenticator.php // ... use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; @@ -486,57 +388,37 @@ egg to return a custom message if someone tries this: curl -H "X-AUTH-TOKEN: ILuvAPIs" http://localhost:8000/ # {"message":"ILuvAPIs is not a real API key: it's just a silly phrase"} -Building a Login Form ---------------------- - -If you're building a login form, use the :class:`Symfony\\Component\\Security\\Guard\\Authenticator\\AbstractFormLoginAuthenticator` -as your base class - it implements a few methods for you. Then, fill in the other -methods just like with the ``TokenAuthenticator``. Outside of Guard, you are still -responsible for creating a route, controller and template for your login form. - -.. _guard-csrf-protection: - -Adding CSRF Protection ----------------------- +.. _guard-manual-auth: -If you're using a Guard authenticator to build a login form and want to add CSRF -protection, no problem! +Manually Authenticating a User +------------------------------ -First, :ref:`add the _csrf_token to your login template `. +Sometimes you might want to manually authenticate a user - like after the user +completes registration. To do that, use your authenticator and a service called +``GuardAuthenticatorHandler``:: -Then, type-hint ``CsrfTokenManagerInterface`` in your ``__construct()`` method -(or manually configure the ``security.csrf.token_manager`` service to be passed) -and add the following logic:: - - // src/AppBundle/Security/ExampleFormAuthenticator.php + // src/Controller/RegistrationController.php // ... - use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; - use Symfony\Component\Security\Csrf\CsrfToken; - use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; - use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; + use App\Security\LoginFormAuthenticator; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; - class ExampleFormAuthenticator extends AbstractFormLoginAuthenticator + class RegistrationController extends AbstractController { - private $csrfTokenManager; - - public function __construct(CsrfTokenManagerInterface $csrfTokenManager) - { - $this->csrfTokenManager = $csrfTokenManager; - } - - public function getCredentials(Request $request) + public function register(LoginFormAuthenticator $authenticator, GuardAuthenticatorHandler $guardHandler, Request $request) { - $csrfToken = $request->request->get('_csrf_token'); - - if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken('authenticate', $csrfToken))) { - throw new InvalidCsrfTokenException('Invalid CSRF token.'); - } + // ... - // ... all your normal logic + // after validating the user and saving them to the database + // authenticate the user and use onAuthenticationSuccess on the authenticator + return $guardHandler->authenticateUserAndHandleSuccess( + $user, // the User object you just created + $request, + $authenticator, // authenticator whose onAuthenticationSuccess you want to use + 'main' // the name of your firewall in security.yaml + ); } - - // ... } Avoid Authenticating the Browser on Every Request @@ -610,7 +492,7 @@ Frequently Asked Questions -------------------------- **Can I have Multiple Authenticators?** - Yes! But when you do, you'll need choose just *one* authenticator to be your + Yes! But when you do, you'll need to choose just *one* authenticator to be your "entry_point". This means you'll need to choose *which* authenticator's ``start()`` method should be called when an anonymous user tries to access a protected resource. For more details, see :doc:`/security/multiple_guard_authenticators`. @@ -628,4 +510,5 @@ Frequently Asked Questions question) or use the ``User`` object from FOSUserBundle and create your own authenticator(s) (just like in this article). -.. _`must be quoted with backticks`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#quoting-reserved-words +.. _`Social Authentication`: https://github.com/knpuniversity/oauth2-client-bundle#authenticating-with-guard +.. _`HWIOAuthBundle`: https://github.com/hwi/HWIOAuthBundle diff --git a/security/host_restriction.rst b/security/host_restriction.rst deleted file mode 100644 index b577a737585..00000000000 --- a/security/host_restriction.rst +++ /dev/null @@ -1,6 +0,0 @@ -How to Restrict Firewalls to a Specific Host -============================================ - -As of Symfony 2.5, more possibilities to restrict firewalls have been added. -You can read everything about all the possibilities (including ``host``) -in ":doc:`/security/firewall_restriction`". diff --git a/security/impersonating_user.rst b/security/impersonating_user.rst index 9c6d44332ce..d9d1deba976 100644 --- a/security/impersonating_user.rst +++ b/security/impersonating_user.rst @@ -5,16 +5,14 @@ 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). +having to log out and log in again (for instance when you are debugging something +a user sees that you can't reproduce). .. caution:: - User impersonation is not compatible with - :doc:`pre authenticated firewalls `. The - reason is that impersonation requires the authentication state to be maintained - server-side, but pre-authenticated information (``SSL_CLIENT_S_DN_Email``, - ``REMOTE_USER`` or other) is sent in each request. + User impersonation is not compatible with some authentication mechanisms + (e.g. ``REMOTE_USER``) where the authentication information is expected to be + sent on each request. Impersonating the user can be done by activating the ``switch_user`` firewall listener: @@ -23,7 +21,7 @@ listener: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -34,7 +32,7 @@ listener: .. code-block:: xml - + loadFromExtension('security', [ // ... @@ -66,13 +64,9 @@ listener: ], ]); -.. tip:: - - For using the ``switch_user`` listener in a ``stateless`` firewall, set the - ``switch_user.stateless`` option to ``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: +To switch to another user, add a query string with the ``_switch_user`` +parameter and the username (or whatever field our user provider uses to load users) +as the value to the current URL: .. code-block:: text @@ -84,9 +78,16 @@ 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: +This feature is only available to users with a special role called ``ROLE_ALLOWED_TO_SWITCH``. +Using :ref:`role_hierarchy ` is a great way to give this +role to the users that need it. + +Knowing When Impersonation Is Active +------------------------------------ + +When a user is being impersonated, Symfony grants them a special role called +``ROLE_PREVIOUS_ADMIN`` (in addition to the roles the user may have). Use this +special role, for instance, to show a link to exit impersonation in a template: .. code-block:: html+twig @@ -94,11 +95,16 @@ to show a link to exit impersonation: Exit impersonation {% endif %} -In some cases you may need to get the object that represents the impersonator -user rather than the impersonated user. Use the following snippet to iterate -over the user's roles until you find one that is a ``SwitchUserRole`` object:: +Finding the Original User +------------------------- + +In some cases, you may need to get the object that represents the impersonator +user rather than the impersonated user. When a user is impersonated the token +stored in the token storage will be a ``SwitchUserToken`` instance. Use the +following snippet to obtain the original token which gives you access to +the impersonator user:: - use Symfony\Component\Security\Core\Role\SwitchUserRole; + use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Security; // ... @@ -115,28 +121,29 @@ over the user's roles until you find one that is a ``SwitchUserRole`` object:: { // ... - if ($this->security->isGranted('ROLE_PREVIOUS_ADMIN')) { - foreach ($this->security->getToken()->getRoles() as $role) { - if ($role instanceof SwitchUserRole) { - $impersonatorUser = $role->getSource()->getUser(); - break; - } - } + $token = $this->security->getToken(); + + if ($token instanceof SwitchUserToken) { + $impersonatorUser = $token->getOriginalToken()->getUser(); } + + // ... } } -This feature needs to be made available to a small group of users. +Controlling the Query Parameter +------------------------------- + +This feature needs to be available only to a restricted 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: +role. The name of this role can be modified via the ``role`` setting. You can +also adjust the query parameter name via the ``parameter`` setting: .. configuration-block:: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -147,7 +154,7 @@ setting: .. code-block:: xml - + loadFromExtension('security', [ // ... @@ -181,6 +188,119 @@ setting: ], ]); +Limiting User Switching +----------------------- + +If you need more control over user switching, you can use a security voter. First, +configure ``switch_user`` to check for some new, custom attribute. This can be +anything, but *cannot* start with ``ROLE_`` (to enforce that only your voter will) +be called: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + switch_user: { role: CAN_SWITCH_USER } + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + // ... + + 'firewalls' => [ + 'main'=> [ + // ... + 'switch_user' => [ + 'role' => 'CAN_SWITCH_USER', + ], + ], + ], + ]); + +Then, create a voter class that responds to this role and includes whatever custom +logic you want:: + + namespace App\Security\Voter; + + use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Authorization\Voter\Voter; + use Symfony\Component\Security\Core\Security; + use Symfony\Component\Security\Core\User\UserInterface; + + class SwitchToCustomerVoter extends Voter + { + private $security; + + public function __construct(Security $security) + { + $this->security = $security; + } + + protected function supports($attribute, $subject) + { + return in_array($attribute, ['CAN_SWITCH_USER']) + && $subject instanceof UserInterface; + } + + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + $user = $token->getUser(); + // if the user is anonymous or if the subject is not a user, do not grant access + if (!$user instanceof UserInterface || !$subject instanceof UserInterface) { + return false; + } + + // you can still check for ROLE_ALLOWED_TO_SWITCH + if ($this->security->isGranted('ROLE_ALLOWED_TO_SWITCH')) { + return true; + } + + // check for any roles you want + if ($this->security->isGranted('ROLE_TECH_SUPPORT')) { + return true; + } + + /* + * or use some custom data from your User object + if ($user->isAllowedToSwitch()) { + return true; + } + */ + + return false; + } + } + +That's it! When switching users, your voter now has full control over whether or +not this is allowed. If your voter isn't called, see :ref:`declaring-the-voter-as-a-service`. + Events ------ @@ -192,8 +312,8 @@ The :doc:`/session/locale_sticky_session` article does not update the locale when you impersonate a user. If you *do* want to be sure to update the locale when you switch users, add an event subscriber on this event:: - // src/AppBundle/EventListener/SwitchUserSubscriber.php - namespace AppBundle\EventListener; + // src/EventListener/SwitchUserSubscriber.php + namespace App\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Http\Event\SwitchUserEvent; @@ -203,11 +323,15 @@ you switch users, add an event subscriber on this event:: { public function onSwitchUser(SwitchUserEvent $event) { - $event->getRequest()->getSession()->set( - '_locale', - // assuming your User has some getLocale() method - $event->getTargetUser()->getLocale() - ); + $request = $event->getRequest(); + + if ($request->hasSession() && ($session = $request->getSession())) { + $session->set( + '_locale', + // assuming your User has some getLocale() method + $event->getTargetUser()->getLocale() + ); + } } public static function getSubscribedEvents() @@ -219,7 +343,7 @@ you switch users, add an event subscriber on this event:: } } -That's it! If you're using the :ref:`default services.yml configuration `, +That's it! If you're using the :ref:`default services.yaml configuration `, Symfony will automatically discover your service and call ``onSwitchUser`` whenever a switch user occurs. diff --git a/security/json_login_setup.rst b/security/json_login_setup.rst index e5945671058..4ca0b8809e6 100644 --- a/security/json_login_setup.rst +++ b/security/json_login_setup.rst @@ -11,7 +11,7 @@ First, enable the JSON login under your firewall: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -23,7 +23,7 @@ First, enable the JSON login under your firewall: .. code-block:: xml - + loadFromExtension('security', [ 'firewalls' => [ 'main' => [ @@ -64,19 +64,19 @@ The next step is to configure a route in your app matching this path: .. code-block:: php-annotations - // src/AppBundle/Controller/SecurityController.php + // src/Controller/SecurityController.php // ... - use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; - class SecurityController extends Controller + class SecurityController extends AbstractController { /** - * @Route("/login", name="login") + * @Route("/login", name="login", methods={"POST"}) */ - public function loginAction(Request $request) + public function login(Request $request) { $user = $this->getUser(); @@ -89,37 +89,36 @@ The next step is to configure a route in your app matching this path: .. code-block:: yaml - # app/config/routing.yml + # config/routes.yaml login: - path: /login - defaults: { _controller: AppBundle:Security:login } + path: /login + controller: App\Controller\SecurityController::login + methods: POST .. code-block:: xml - + - - AppBundle:Security:login - + .. code-block:: php - // app/config/routing.php - use Symfony\Component\Routing\Route; - use Symfony\Component\Routing\RouteCollection; + // config/routes.php + use App\Controller\SecurityController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - $routes = new RouteCollection(); - $routes->add('login', new Route('/login', [ - '_controller' => 'AppBundle:Security:login', - ])); - - return $routes; + return function (RoutingConfigurator $routes) { + $routes->add('login', '/login') + ->controller([SecurityController::class, 'login']) + ->methods(['POST']) + ; + }; Now, when you make a ``POST`` request, with the header ``Content-Type: application/json``, to the ``/login`` URL with the following JSON document as the body, the security @@ -158,7 +157,7 @@ The security configuration should be: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -172,7 +171,7 @@ The security configuration should be: .. code-block:: xml - + loadFromExtension('security', [ 'firewalls' => [ 'main' => [ diff --git a/security/ldap.rst b/security/ldap.rst index 2b73e3a9f3b..f29909dbc4a 100644 --- a/security/ldap.rst +++ b/security/ldap.rst @@ -8,8 +8,8 @@ Symfony provides different means to work with an LDAP server. The Security component offers: -* The ``ldap`` user provider, using the - :class:`Symfony\\Component\\Security\\Core\\User\\LdapUserProvider` +* The ``ldap`` :doc:`user provider`, using the + :class:`Symfony\\Component\\Ldap\\Security\\LdapUserProvider` class. Like all other user providers, it can be used with any authentication provider. @@ -34,6 +34,16 @@ This means that the following scenarios will work: * Loading user information from an LDAP server, while using another authentication strategy (token-based pre-authentication, for example). +Installation +------------ + +In applications using :ref:`Symfony Flex `, run this command to +install the Ldap component before using it: + +.. code-block:: terminal + + $ composer require symfony/ldap + Ldap Configuration Reference ---------------------------- @@ -49,14 +59,14 @@ The providers are configured to use a default service named ``ldap``, but you can override this setting in the security component's configuration. -An LDAP client can be simply configured using the built-in +An LDAP client can be configured using the built-in `LDAP PHP extension`_ with the following service definition: .. configuration-block:: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: Symfony\Component\Ldap\Ldap: arguments: ['@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter'] @@ -71,7 +81,7 @@ An LDAP client can be simply configured using the built-in .. code-block:: xml - + + loadFromExtension('security', [ @@ -178,6 +190,7 @@ use the ``ldap`` user provider. 'search_password' => 'password', 'default_roles' => 'ROLE_USER', 'uid_key' => 'uid', + 'extra_fields' => ['email'], ], ], ], @@ -201,8 +214,8 @@ use the ``ldap`` user provider. The ``ldap`` user provider supports many different configuration options: -``service`` -........... +service +....... **type**: ``string`` **default**: ``ldap`` @@ -210,31 +223,31 @@ This is the name of your configured LDAP client. You can freely choose the name, but it must be unique in your application and it cannot start with a number or contain white spaces. -``base_dn`` -........... +base_dn +....... **type**: ``string`` **default**: ``null`` This is the base DN for the directory -``search_dn`` -............. +search_dn +......... **type**: ``string`` **default**: ``null`` This is your read-only user's DN, which will be used to authenticate against the LDAP server in order to fetch the user's information. -``search_password`` -................... +search_password +............... **type**: ``string`` **default**: ``null`` This is your read-only user's password, which will be used to authenticate against the LDAP server in order to fetch the user's information. -``default_roles`` -................. +default_roles +............. **type**: ``array`` **default**: ``[]`` @@ -242,22 +255,33 @@ This is the default role you wish to give to a user fetched from the LDAP server. If you do not configure this key, your users won't have any roles, and will not be considered as authenticated fully. -``uid_key`` -........... +uid_key +....... -**type**: ``string`` **default**: ``sAMAccountName`` +**type**: ``string`` **default**: ``null`` This is the entry's key to use as its UID. Depends on your LDAP server implementation. Commonly used values are: -* ``sAMAccountName`` +* ``sAMAccountName`` (default) * ``userPrincipalName`` * ``uid`` -``filter`` -.......... +If you pass ``null`` as the value of this option, the default UID key is used +``sAMAccountName``. + +extra_fields +............ + +**type**: ``array`` **default**: ``null`` -**type**: ``string`` **default**: ``({uid_key}={username})`` +Defines the custom fields to pull from the LDAP server. If any field does not +exist, an ``\InvalidArgumentException`` will be thrown. + +filter +...... + +**type**: ``string`` **default**: ``null`` This key lets you configure which LDAP query will be used. The ``{uid_key}`` string will be replaced by the value of the ``uid_key`` configuration value @@ -267,6 +291,9 @@ replaced by the username you are trying to load. For example, with a ``uid_key`` of ``uid``, and if you are trying to load the user ``fabpot``, the final string will be: ``(uid=fabpot)``. +If you pass ``null`` as the value of this option, the default filter is used +``({uid_key}={username})``. + In order to prevent `LDAP injection`_, the username will be escaped. The syntax for the ``filter`` key is defined by `RFC4515`_. @@ -334,7 +361,7 @@ Configuration example for form login .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -348,7 +375,7 @@ Configuration example for form login .. code-block:: xml - + loadFromExtension('security', [ @@ -388,7 +416,7 @@ Configuration example for HTTP Basic .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -402,7 +430,7 @@ Configuration example for HTTP Basic .. code-block:: xml - + loadFromExtension('security', [ @@ -441,7 +470,7 @@ Configuration example for form login and query_string .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -453,10 +482,12 @@ Configuration example for form login and query_string service: Symfony\Component\Ldap\Ldap dn_string: 'dc=example,dc=com' query_string: '(&(uid={username})(memberOf=cn=users,ou=Services,dc=example,dc=com))' + search_dn: '...' + search_password: 'the-raw-password' .. code-block:: xml - + + query-string="(&(uid={username})(memberOf=cn=users,ou=Services,dc=example,dc=com))" + search-dn="..." + search-password="the-raw-password"/> .. code-block:: php - // app/config/security.php + // config/packages/security.php use Symfony\Component\Ldap\Ldap; $container->loadFromExtension('security', [ @@ -486,6 +519,8 @@ Configuration example for form login and query_string 'service' => Ldap::class, 'dn_string' => 'dc=example,dc=com', 'query_string' => '(&(uid={username})(memberOf=cn=users,ou=Services,dc=example,dc=com))', + 'search_dn' => '...', + 'search_password' => 'the-raw-password', // ... ], ], diff --git a/security/multiple_guard_authenticators.rst b/security/multiple_guard_authenticators.rst index 08486591eb5..356015681b0 100644 --- a/security/multiple_guard_authenticators.rst +++ b/security/multiple_guard_authenticators.rst @@ -22,7 +22,7 @@ This is how your security configuration can look in action: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... firewalls: @@ -30,13 +30,13 @@ This is how your security configuration can look in action: anonymous: ~ guard: authenticators: - - AppBundle\Security\LoginFormAuthenticator - - AppBundle\Security\FacebookConnectAuthenticator - entry_point: AppBundle\Security\LoginFormAuthenticator + - App\Security\LoginFormAuthenticator + - App\Security\FacebookConnectAuthenticator + entry_point: App\Security\LoginFormAuthenticator .. code-block:: xml - + - - AppBundle\Security\LoginFormAuthenticator - AppBundle\Security\FacebookConnectAuthenticator + + App\Security\LoginFormAuthenticator + App\Security\FacebookConnectAuthenticator @@ -58,9 +58,9 @@ This is how your security configuration can look in action: .. code-block:: php - // app/config/security.php - use AppBundle\Security\FacebookConnectAuthenticator; - use AppBundle\Security\LoginFormAuthenticator; + // config/packages/security.php + use App\Security\FacebookConnectAuthenticator; + use App\Security\LoginFormAuthenticator; $container->loadFromExtension('security', [ // ... @@ -68,7 +68,7 @@ This is how your security configuration can look in action: 'default' => [ 'anonymous' => null, 'guard' => [ - 'entry_point' => '', + 'entry_point' => LoginFormAuthenticator::class, 'authenticators' => [ LoginFormAuthenticator::class, FacebookConnectAuthenticator::class, @@ -93,7 +93,7 @@ the solution is to split the configuration into two separate firewalls: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... firewalls: @@ -101,12 +101,12 @@ the solution is to split the configuration into two separate firewalls: pattern: ^/api/ guard: authenticators: - - AppBundle\Security\ApiTokenAuthenticator + - App\Security\ApiTokenAuthenticator default: anonymous: ~ guard: authenticators: - - AppBundle\Security\LoginFormAuthenticator + - App\Security\LoginFormAuthenticator access_control: - { path: '^/login', roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: '^/api', roles: ROLE_API_USER } @@ -114,7 +114,7 @@ the solution is to split the configuration into two separate firewalls: .. code-block:: xml - + - AppBundle\Security\ApiTokenAuthenticator + App\Security\ApiTokenAuthenticator - AppBundle\Security\LoginFormAuthenticator + App\Security\LoginFormAuthenticator @@ -143,9 +143,9 @@ the solution is to split the configuration into two separate firewalls: .. code-block:: php - // app/config/security.php - use AppBundle\Security\ApiTokenAuthenticator; - use AppBundle\Security\LoginFormAuthenticator; + // config/packages/security.php + use App\Security\ApiTokenAuthenticator; + use App\Security\LoginFormAuthenticator; $container->loadFromExtension('security', [ // ... diff --git a/security/multiple_user_providers.rst b/security/multiple_user_providers.rst deleted file mode 100644 index f228d0839a1..00000000000 --- a/security/multiple_user_providers.rst +++ /dev/null @@ -1,187 +0,0 @@ -How to Use multiple User Providers -================================== - -.. note:: - - It's always better to use a specific user provider for each authentication - mechanism. Chaining user providers should be avoided in most applications - and used only to solve edge cases. - -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: - -.. configuration-block:: - - .. code-block:: 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: AppBundle\Entity\User, property: username } - - .. code-block:: xml - - - - - - - - - in_memory - user_db - - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - use AppBundle\Entity\User; - - $container->loadFromExtension('security', [ - 'providers' => [ - 'chain_provider' => [ - 'chain' => [ - 'providers' => ['in_memory', 'user_db'], - ], - ], - 'in_memory' => [ - 'memory' => [ - 'users' => [ - 'foo' => ['password' => 'test'], - ], - ], - ], - 'user_db' => [ - 'entity' => [ - 'class' => User::class, - 'property' => 'username', - ], - ], - ], - ]); - -Now, all firewalls that explicitly define ``chain_provider`` as their user -provider will, in turn, try to load the user from both the ``in_memory`` and -``user_db`` providers. - -.. deprecated:: 3.4 - - In previous Symfony versions, firewalls that didn't define their user provider - explicitly, used the first existing provider (``chain_provider`` in this - example). However, auto-selecting the first user provider has been deprecated - in Symfony 3.4 and will throw an exception in 4.0. Always define the provider - used by the firewall when there are multiple 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: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - secured_area: - # ... - pattern: ^/ - provider: user_db - http_basic: - realm: 'Secured Demo Area' - provider: in_memory - form_login: ~ - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'secured_area' => [ - // ... - 'pattern' => '^/', - 'provider' => 'user_db', - 'http_basic' => [ - // ... - 'realm' => 'Secured Demo Area', - 'provider' => 'in_memory', - ], - 'form_login' => [], - ], - ], - ]); - -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). - -If you need to check that the user being returned by your provider is a allowed -to authenticate, check the returned user object:: - - use Symfony\Component\Security\Core\User; - // ... - - public function loadUserByUsername($username) - { - // ... - - // you can, for example, test that the returned user is an object of a - // particular class or check for certain attributes of your user objects - if ($user instance User) { - // the user was loaded from the main security config file. Do something. - // ... - } - - return $user; - } - -For more information about user provider and firewall configuration, see -the :doc:`/reference/configuration/security`. diff --git a/security/named_encoders.rst b/security/named_encoders.rst index 803f3d16992..aad4d740fb1 100644 --- a/security/named_encoders.rst +++ b/security/named_encoders.rst @@ -1,8 +1,8 @@ .. index:: single: Security; Named Encoders -How to Choose the Password Encoder Algorithm Dynamically -======================================================== +How to Use A Different Password Encoder Algorithm Per User +========================================================== Usually, the same password encoder is used for all users by configuring it to apply to all instances of a specific class: @@ -11,15 +11,17 @@ to apply to all instances of a specific class: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... encoders: - Symfony\Component\Security\Core\User\User: sha512 + App\Entity\User: + algorithm: auto + cost: 12 .. code-block:: xml - + - .. code-block:: php - // app/config/security.php - use Symfony\Component\Security\Core\User\User; + // config/packages/security.php + use App\Entity\User; $container->loadFromExtension('security', [ // ... 'encoders' => [ User::class => [ - 'algorithm' => 'sha512', + 'algorithm' => 'auto', + 'cost' => 12, ], ], ]); @@ -52,26 +56,26 @@ to apply to all instances of a specific class: Another option is to use a "named" encoder and then select which encoder you want to use dynamically. -In the previous example, you've set the ``sha512`` algorithm for ``Acme\UserBundle\Entity\User``. +In the previous example, you've set the ``auto`` algorithm for ``App\Entity\User``. This may be secure enough for a regular user, but what if you want your admins -to have a stronger algorithm, for example ``bcrypt``. This can be done with -named encoders: +to have a stronger algorithm, for example ``auto`` with a higher cost. This can +be done with named encoders: .. configuration-block:: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... encoders: harsh: - algorithm: bcrypt + algorithm: auto cost: 15 .. code-block:: xml - + .. code-block:: php - // app/config/security.php + // config/packages/security.php $container->loadFromExtension('security', [ // ... 'encoders' => [ 'harsh' => [ - 'algorithm' => 'bcrypt', + 'algorithm' => 'auto', 'cost' => '15', ], ], @@ -105,7 +109,7 @@ named encoders: If you are running PHP 7.2+ or have the `libsodium`_ extension installed, then the recommended hashing algorithm to use is - :ref:`Argon2i `. + :ref:`Sodium `. This creates an encoder named ``harsh``. In order for a ``User`` instance to use it, the class must implement @@ -139,16 +143,16 @@ you must register a service for it in order to use it as a named encoder: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... encoders: app_encoder: - id: 'app.password_encoder_service' + id: 'App\Security\Encoder\MyCustomPasswordEncoder' .. code-block:: xml - + + id="App\Security\Encoder\MyCustomPasswordEncoder"/> .. code-block:: php - // app/config/security.php + // config/packages/security.php + // ... + use App\Security\Encoder\MyCustomPasswordEncoder; + $container->loadFromExtension('security', [ // ... 'encoders' => [ 'app_encoder' => [ - 'id' => 'app.password_encoder_service', + 'id' => MyCustomPasswordEncoder::class, ], ], ]); -This creates an encoder named ``app_encoder`` from a service named -``app.password_encoder_service``. +This creates an encoder named ``app_encoder`` from a service with the ID +``App\Security\Encoder\MyCustomPasswordEncoder``. .. _`libsodium`: https://pecl.php.net/package/libsodium diff --git a/security/password_encoding.rst b/security/password_encoding.rst deleted file mode 100644 index 0cad90600d7..00000000000 --- a/security/password_encoding.rst +++ /dev/null @@ -1,45 +0,0 @@ -.. index:: - single: Security; Encoding Passwords - -How to Manually Encode a Password -================================= - -.. note:: - - For historical reasons, Symfony uses the term *"password encoding"* when it - should really refer to *"password hashing"*. The "encoders" are in fact - `cryptographic hash functions`_. - -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:: - - use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; - - public function registerAction(UserPasswordEncoderInterface $encoder) - { - // whatever *your* User object is - $user = new AppBundle\Entity\User(); - $plainPassword = 'ryanpass'; - $encoded = $encoder->encodePassword($user, $plainPassword); - - $user->setPassword($encoded); - } - -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. - -.. caution:: - - 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 - :ref:`How to implement a simple Registration Form `. - -.. _`cryptographic hash functions`: https://en.wikipedia.org/wiki/Cryptographic_hash_function diff --git a/security/password_migration.rst b/security/password_migration.rst new file mode 100644 index 00000000000..912ded38d0e --- /dev/null +++ b/security/password_migration.rst @@ -0,0 +1,240 @@ +.. index:: + single: Security; How to Migrate a Password Hash + +How to Migrate a Password Hash +============================== + +In order to protect passwords, it is recommended to store them using the latest +hash algorithms. This means that if a better hash algorithm is supported on your +system, the user's password should be *rehashed* using the newer algorithm and +stored. That's possible with the ``migrate_from`` option: + +#. `Configure a new Encoder Using "migrate_from"`_ +#. `Upgrade the Password`_ +#. Optionally, `Trigger Password Migration From a Custom Encoder`_ + +Configure a new Encoder Using "migrate_from" +---------------------------------------------- + +When a better hashing algorithm becomes available, you should keep the existing +encoder(s), rename it, and then define the new one. Set the ``migrate_from`` option +on the new encoder to point to the old, legacy encoder(s): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + encoders: + # an encoder used in the past for some users + legacy: + algorithm: sha256 + encode_as_base64: false + iterations: 1 + + App\Entity\User: + # the new encoder, along with its options + algorithm: sodium + migrate_from: + - bcrypt # uses the "bcrypt" encoder with the default options + - legacy # uses the "legacy" encoder configured above + + .. code-block:: xml + + + + + + + + + + + + + + bcrypt + + + legacy + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + // ... + + 'encoders' => [ + 'legacy' => [ + 'algorithm' => 'sha256', + 'encode_as_base64' => false, + 'iterations' => 1, + ], + + 'App\Entity\User' => [ + // the new encoder, along with its options + 'algorithm' => 'sodium', + 'migrate_from' => [ + 'bcrypt', // uses the "bcrypt" encoder with the default options + 'legacy', // uses the "legacy" encoder configured above + ], + ], + ], + ]); + +With this setup: + +* New users will be encoded with the new algorithm; +* Whenever a user logs in whose password is still stored using the old algorithm, + Symfony will verify the password with the old algorithm and then rehash + and update the password using the new algorithm. + +.. tip:: + + The *auto*, *native*, *bcrypt* and *argon* encoders automatically enable + password migration using the following list of ``migrate_from`` algorithms: + + #. :ref:`PBKDF2 ` (which uses :phpfunction:`hash_pbkdf2`); + #. Message digest (which uses :phpfunction:`hash`) + + Both use the ``hash_algorithm`` setting as the algorithm. It is recommended to + use ``migrate_from`` instead of ``hash_algorithm``, unless the *auto* + encoder is used. + +Upgrade the Password +-------------------- + +Upon successful login, the Security system checks whether a better algorithm +is available to hash the user's password. If it is, it'll hash the correct +password using the new hash. If you use a Guard authenticator, you first need to +`provide the original password to the Security system `_. + +You can enable the upgrade behavior by implementing how this newly hashed +password should be stored: + +* `When using Doctrine's entity user provider `_ +* `When using a custom user provider `_ + +After this, you're done and passwords are always hashed as secure as possible! + +Provide the Password when using Guard +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you're using a custom :doc:`guard authenticator `, +you need to implement :class:`Symfony\\Component\\Security\\Guard\\PasswordAuthenticatedInterface`. +This interface defines a ``getPassword()`` method that returns the password +for this login request. This password is used in the migration process:: + + // src/Security/CustomAuthenticator.php + namespace App\Security; + + use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; + // ... + + class CustomAuthenticator extends AbstractGuardAuthenticator implements PasswordAuthenticatedInterface + { + // ... + + public function getPassword($credentials): ?string + { + return $credentials['password']; + } + } + +Upgrade the Password when using Doctrine +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using the :ref:`entity user provider `, implement +:class:`Symfony\\Component\\Security\\Core\\User\\PasswordUpgraderInterface` in +the ``UserRepository`` (see `the Doctrine docs for information`_ on how to +create this class if it's not already created). This interface implements +storing the newly created password hash:: + + // src/Repository/UserRepository.php + namespace App\Repository; + + // ... + use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; + + class UserRepository extends EntityRepository implements PasswordUpgraderInterface + { + // ... + + public function upgradePassword(UserInterface $user, string $newEncodedPassword): void + { + // set the new encoded password on the User object + $user->setPassword($newEncodedPassword); + + // execute the queries on the database + $this->getEntityManager()->flush($user); + } + } + +Upgrade the Password when using a Custom User Provider +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you're using a :ref:`custom user provider `, implement the +:class:`Symfony\\Component\\Security\\Core\\User\\PasswordUpgraderInterface` in +the user provider:: + + // src/Security/UserProvider.php + namespace App\Security; + + // ... + use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; + + class UserProvider implements UserProviderInterface, PasswordUpgraderInterface + { + // ... + + public function upgradePassword(UserInterface $user, string $newEncodedPassword): void + { + // set the new encoded password on the User object + $user->setPassword($newEncodedPassword); + + // ... store the new password + } + } + +Trigger Password Migration From a Custom Encoder +------------------------------------------------ + +If you're using a custom password encoder, you can trigger the password +migration by returning ``true`` in the ``needsRehash()`` method:: + + // src/Security/CustomPasswordEncoder.php + namespace App\Security; + + // ... + use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; + + class CustomPasswordEncoder implements PasswordEncoderInterface + { + // ... + + public function needsRehash(string $encoded): bool + { + // check whether the current password is hash using an outdated encoder + $hashIsOutdated = ...; + + return $hashIsOutdated; + } + } + +.. _`the Doctrine docs for information`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-objects.html#custom-repositories diff --git a/security/pre_authenticated.rst b/security/pre_authenticated.rst deleted file mode 100644 index de433454960..00000000000 --- a/security/pre_authenticated.rst +++ /dev/null @@ -1,159 +0,0 @@ -.. index:: - single: Security; Pre authenticated providers - -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. - -.. caution:: - - :doc:`User impersonation ` is not - compatible with pre-authenticated firewalls. The reason is that - impersonation requires the authentication state to be maintained server-side, - but pre-authenticated information (``SSL_CLIENT_S_DN_Email``, ``REMOTE_USER`` - or other) is sent in each request. - -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: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - firewalls: - secured_area: - pattern: ^/ - x509: - provider: your_user_provider - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', [ - // ... - - 'firewalls' => [ - 'secured_area' => [ - 'pattern' => '^/', - 'x509' => [ - '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 -:class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\PreAuthenticatedToken`. -You can override these by setting the ``user`` and the ``credentials`` keys -in the x509 firewall configuration respectively. - -.. _security-pre-authenticated-user-provider-note: - -.. note:: - - 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: - - * :doc:`/security/custom_provider` - * :doc:`/security/entity_provider` - -REMOTE_USER Based Authentication --------------------------------- - -A lot of authentication modules, like ``auth_kerb`` for Apache provide the username -using the ``REMOTE_USER`` environment variable. This variable can be trusted by -the application since the authentication happened before the request reached it. - -To configure Symfony using the ``REMOTE_USER`` environment variable, simply enable the -corresponding firewall in your security configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - firewalls: - secured_area: - pattern: ^/ - remote_user: - provider: your_user_provider - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'secured_area' => [ - 'pattern' => '^/', - 'remote_user' => [ - 'provider' => 'your_user_provider', - ], - ], - ], - ]); - -The firewall will then provide the ``REMOTE_USER`` environment variable to -your user provider. You can change the variable name used by setting the ``user`` -key in the ``remote_user`` firewall configuration. - -.. note:: - - Just like for X509 authentication, you will need to configure a "user provider". - See :ref:`the previous note ` - for more information. - diff --git a/security/remember_me.rst b/security/remember_me.rst index 698b53f4f57..f96bb7bbe9e 100644 --- a/security/remember_me.rst +++ b/security/remember_me.rst @@ -14,7 +14,7 @@ the session lasts using a cookie with the ``remember_me`` firewall option: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -22,7 +22,7 @@ the session lasts using a cookie with the ``remember_me`` firewall option: main: # ... remember_me: - secret: '%secret%' + secret: '%kernel.secret%' lifetime: 604800 # 1 week in seconds path: / # by default, the feature is enabled by checking a @@ -32,7 +32,7 @@ the session lasts using a cookie with the ``remember_me`` firewall option: .. code-block:: xml - + + register(DoctrineTokenProvider::class); @@ -320,7 +254,7 @@ service you just created: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: # ... @@ -333,7 +267,7 @@ service you just created: .. code-block:: xml - + loadFromExtension('security', [ // ... diff --git a/security/securing_services.rst b/security/securing_services.rst index d4cf8e0dce4..67b37dd792e 100644 --- a/security/securing_services.rst +++ b/security/securing_services.rst @@ -5,65 +5,37 @@ How to Secure any Service or Method in your Application ======================================================= -In the security article, you can see how to -:ref:`secure a controller ` by requesting -the ``security.authorization_checker`` service from the Service Container and -checking the current user's role:: +In the security article, you learned how to +:ref:`secure a controller ` via a shortcut method. - // ... - use Symfony\Component\Security\Core\Exception\AccessDeniedException; - - public function helloAction($name) - { - $this->denyAccessUnlessGranted('ROLE_ADMIN'); - - // ... - } - -You can also secure *any* service by injecting the ``security.authorization_checker`` -service into it. For a general introduction to injecting dependencies into -services see the :doc:`/service_container` article. 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 - } - - // ... - } +But, you can check access *anywhere* in your code by injecting the ``Security`` +service. For example, suppose you have a ``SalesReportManager`` service and you +want to include extra details only for users that have a ``ROLE_SALES_ADMIN`` role: -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.helper`` service -using the :class:`Symfony\\Component\\Security\\Core\\Security` class:: +.. code-block:: diff - // src/AppBundle/Newsletter/NewsletterManager.php + // src/Newsletter/NewsletterManager.php // ... use Symfony\Component\Security\Core\Exception\AccessDeniedException; - use Symfony\Component\Security\Core\Security; + + use Symfony\Component\Security\Core\Security; - class NewsletterManager + class SalesReportManager { - protected $security; + + private $security; - public function __construct(Security $security) - { - $this->security = $security; - } + + public function __construct(Security $security) + + { + + $this->security = $security; + + } public function sendNewsletter() { - if (!$this->security->isGranted('ROLE_NEWSLETTER_ADMIN')) { - throw new AccessDeniedException(); - } + $salesData = []; + + + if ($this->security->isGranted('ROLE_SALES_ADMIN')) { + + $salesData['top_secret_numbers'] = rand(); + + } // ... } @@ -71,21 +43,11 @@ using the :class:`Symfony\\Component\\Security\\Core\\Security` class:: // ... } -If you're using the :ref:`default services.yml configuration `, +If you're using the :ref:`default services.yaml configuration `, Symfony will automatically pass the ``security.helper`` to your service thanks to autowiring and the ``Security`` type-hint. -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. - -See the `JMSSecurityExtraBundle Documentation`_ for more details. - -.. _`JMSSecurityExtraBundle`: https://github.com/schmittjoh/JMSSecurityExtraBundle -.. _`JMSSecurityExtraBundle Documentation`: http://jmsyst.com/bundles/JMSSecurityExtraBundle +You can also use a lower-level +:class:`Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationCheckerInterface` +service. It does the same thing as ``Security``, but allows you to type-hint a +more-specific interface. diff --git a/security/security_checker.rst b/security/security_checker.rst deleted file mode 100644 index dbf4b0d2ee7..00000000000 --- a/security/security_checker.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. index:: - single: Security; Vulnerability Checker - -How to Check for Known Security Vulnerabilities in Your Dependencies -==================================================================== - -When using lots of dependencies in your Symfony projects, some of them may -contain security vulnerabilities. That's why the :doc:`Symfony local server ` -includes a command called ``check:security`` that checks your ``composer.lock`` -file to find known security vulnerabilities in your installed dependencies: - -.. code-block:: terminal - - $ symfony check:security - -A good security practice is to execute this command regularly to be able to -update or replace compromised dependencies as soon as possible. The security -check is done locally by cloning the `security advisories database`_ published -by the FriendsOfPHP organization, so your ``composer.lock`` file is not sent on -the network. - -.. tip:: - - The ``check:security`` command terminates with a non-zero exit code if - any of your dependencies is affected by a known security vulnerability. - This way you can add it to your project build process and your continuous - integration workflows to make them fail when there are vulnerabilities. - -.. _`security advisories database`: https://github.com/FriendsOfPHP/security-advisories diff --git a/security/user_checkers.rst b/security/user_checkers.rst index e32bc547acd..c188139d9ec 100644 --- a/security/user_checkers.rst +++ b/security/user_checkers.rst @@ -18,10 +18,10 @@ perform checks before and after user authentication. If one or more conditions are not met, an exception should be thrown which extends the :class:`Symfony\\Component\\Security\\Core\\Exception\\AccountStatusException`:: - namespace AppBundle\Security; + namespace App\Security; - use AppBundle\Exception\AccountDeletedException; - use AppBundle\Security\User as AppUser; + use App\Exception\AccountDeletedException; + use App\Security\User as AppUser; use Symfony\Component\Security\Core\Exception\AccountExpiredException; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -36,7 +36,7 @@ are not met, an exception should be thrown which extends the // user is deleted, show a generic Account Not Found message. if ($user->isDeleted()) { - throw new AccountDeletedException('...'); + throw new AccountDeletedException(); } } @@ -57,7 +57,7 @@ Enabling the Custom User Checker -------------------------------- Next, make sure your user checker is registered as a service. If you're using the -:ref:`default services.yml configuration `, +:ref:`default services.yaml configuration `, the service is registered automatically. All that's left to do is add the checker to the desired firewall where the value @@ -67,19 +67,19 @@ is the service id of your user checker: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml # ... security: firewalls: main: pattern: ^/ - user_checker: AppBundle\Security\UserChecker + user_checker: App\Security\UserChecker # ... .. code-block:: xml - + - AppBundle\Security\UserChecker + App\Security\UserChecker @@ -98,10 +98,10 @@ is the service id of your user checker: .. code-block:: php - // app/config/security.php + // config/packages/security.php // ... - use AppBundle\Security\UserChecker; + use App\Security\UserChecker; $container->loadFromExtension('security', [ 'firewalls' => [ @@ -112,8 +112,3 @@ is the service id of your user checker: ], ], ]); - -.. tip:: - - It's also possible to have a different user checker for each firewall. Use - the ``user_checker`` option under each firewall to choose the one you want. diff --git a/security/user_provider.rst b/security/user_provider.rst new file mode 100644 index 00000000000..45f4911f762 --- /dev/null +++ b/security/user_provider.rst @@ -0,0 +1,512 @@ +Security User Providers +======================= + +User providers are PHP classes related to Symfony Security that have two jobs: + +**Reload the User from the Session** + At the beginning of each request (unless your firewall is ``stateless``), Symfony + loads the ``User`` object from the session. To make sure it's not out-of-date, + the user provider "refreshes it". The Doctrine user provider, for example, + queries the database for fresh data. Symfony then checks to see if the user + has "changed" and de-authenticates the user if they have (see :ref:`user_session_refresh`). + +**Load the User for some Feature** + Some features, like :doc:`user impersonation `, + :doc:`Remember Me ` and many of the built-in + :doc:`authentication providers `, use the user provider + to load a User object via its "username" (or email, or whatever field you want). + +Symfony comes with several built-in user providers: + +* :ref:`Entity User Provider ` (loads users from + a database); +* :ref:`LDAP User Provider ` (loads users from a + LDAP server); +* :ref:`Memory User Provider ` (loads users from + a configuration file); +* :ref:`Chain User Provider ` (merges two or more + user providers into a new user provider). + +The built-in user providers cover all the needs for most applications, but you +can also create your own :ref:`custom user provider `. + +.. _security-entity-user-provider: + +Entity User Provider +-------------------- + +This is the most common user provider for traditional web applications. Users +are stored in a database and the user provider uses :doc:`Doctrine ` +to retrieve them: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + # ... + + providers: + users: + entity: + # the class of the entity that represents users + class: 'App\Entity\User' + # the property to query by - e.g. username, email, etc + property: 'username' + # optional: if you're using multiple Doctrine entity + # managers, this option defines which one to use + # manager_name: 'customer' + + # ... + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + use App\Entity\User; + + $container->loadFromExtension('security', [ + 'providers' => [ + 'users' => [ + 'entity' => [ + // the class of the entity that represents users + 'class' => User::class, + // the property to query by - e.g. username, email, etc + 'property' => 'username', + // optional: if you're using multiple Doctrine entity + // managers, this option defines which one to use + // 'manager_name' => 'customer', + ], + ], + ], + + // ... + ]); + +The ``providers`` section creates a "user provider" called ``users`` that knows +how to query from your ``App\Entity\User`` entity by the ``username`` property. +You can choose any name for the user provider, but it's recommended to pick a +descriptive name because this will be later used in the firewall configuration. + +.. _authenticating-someone-with-a-custom-entity-provider: + +Using a Custom Query to Load the User +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``entity`` provider can only query from one *specific* field, specified by +the ``property`` config key. If you want a bit more control over this - e.g. you +want to find a user by ``email`` *or* ``username``, you can do that by making +your ``UserRepository`` implement the +:class:`Symfony\\Bridge\\Doctrine\\Security\\User\\UserLoaderInterface`. This +interface only requires one method: ``loadUserByUsername($username)``:: + + // src/Repository/UserRepository.php + namespace App\Repository; + + use Doctrine\ORM\EntityRepository; + use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface; + + class UserRepository extends EntityRepository implements UserLoaderInterface + { + // ... + + public function loadUserByUsername($usernameOrEmail) + { + return $this->createQuery( + 'SELECT u + FROM App\Entity\User u + WHERE u.username = :query + OR u.email = :query' + ) + ->setParameter('query', $usernameOrEmail) + ->getQuery() + ->getOneOrNullResult(); + } + } + +To finish this, remove the ``property`` key from the user provider in +``security.yaml``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + providers: + users: + entity: + class: App\Entity\User + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + use App\Entity\User; + + $container->loadFromExtension('security', [ + // ... + + 'providers' => [ + 'users' => [ + 'entity' => [ + 'class' => User::class, + ], + ], + ], + ]); + +This tells Symfony to *not* query automatically for the User. Instead, when +needed (e.g. because :doc:`user impersonation `, +:doc:`Remember Me `, or some other security feature is +activated), the ``loadUserByUsername()`` method on ``UserRepository`` will be called. + +.. _security-memory-user-provider: + +Memory User Provider +-------------------- + +It's not recommended to use this provider in real applications because of its +limitations and how difficult it is to manage users. It may be useful in application +prototypes and for limited applications that don't store users in databases. + +This user provider stores all user information in a configuration file, +including their passwords. That's why the first step is to configure how these +users will encode their passwords: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + encoders: + # this internal class is used by Symfony to represent in-memory users + Symfony\Component\Security\Core\User\User: 'auto' + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + + // this internal class is used by Symfony to represent in-memory users + use Symfony\Component\Security\Core\User\User; + + $container->loadFromExtension('security', [ + // ... + 'encoders' => [ + User::class => [ + 'algorithm' => 'auto', + ], + ], + ]); + +Then, run this command to encode the plain text passwords of your users: + +.. code-block:: terminal + + $ php bin/console security:encode-password + +Now you can configure all the user information in ``config/packages/security.yaml``: + +.. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + providers: + backend_users: + memory: + users: + john_admin: { password: '$2y$13$jxGxc ... IuqDju', roles: ['ROLE_ADMIN'] } + jane_admin: { password: '$2y$13$PFi1I ... rGwXCZ', roles: ['ROLE_ADMIN', 'ROLE_SUPER_ADMIN'] } + +.. _security-ldap-user-provider: + +LDAP User Provider +------------------ + +This user provider requires installing certain dependencies and using some +special authentication providers, so it's explained in a separate article: +:doc:`/security/ldap`. + +.. _security-chain-user-provider: + +Chain User Provider +------------------- + +This user provider combines two or more of the other provider types (``entity``, +``memory`` and ``ldap``) to create a new user provider. The order in which +providers are configured is important because Symfony will look for users +starting from the first provider and will keep looking for in the other +providers until the user is found: + +.. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + providers: + backend_users: + memory: + # ... + + legacy_users: + entity: + # ... + + users: + entity: + # ... + + all_users: + chain: + providers: ['legacy_users', 'users', 'backend'] + +.. _custom-user-provider: + +Creating a Custom User Provider +------------------------------- + +Most applications don't need to create a custom provider. If you store users in +a database, a LDAP server or a configuration file, Symfony supports that. +However, if you're loading users from a custom location (e.g. via an API or +legacy database connection), you'll need to create a custom user provider. + +First, make sure you've followed the :doc:`Security Guide ` to create +your ``User`` class. + +If you used the ``make:user`` command to create your ``User`` class (and you +answered the questions indicating that you need a custom user provider), that +command will generate a nice skeleton to get you started:: + + // src/Security/UserProvider.php + namespace App\Security; + + use Symfony\Component\Security\Core\Exception\UnsupportedUserException; + use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; + use Symfony\Component\Security\Core\User\UserInterface; + use Symfony\Component\Security\Core\User\UserProviderInterface; + + class UserProvider implements UserProviderInterface + { + /** + * Symfony calls this method if you use features like switch_user + * or remember_me. + * + * If you're not using these features, you do not need to implement + * this method. + * + * @return UserInterface + * + * @throws UsernameNotFoundException if the user is not found + */ + public function loadUserByUsername($username) + { + // Load a User object from your data source or throw UsernameNotFoundException. + // The $username argument may not actually be a username: + // it is whatever value is being returned by the getUsername() + // method in your User class. + throw new \Exception('TODO: fill in loadUserByUsername() inside '.__FILE__); + } + + /** + * Refreshes the user after being reloaded from the session. + * + * When a user is logged in, at the beginning of each request, the + * User object is loaded from the session and then this method is + * called. Your job is to make sure the user's data is still fresh by, + * for example, re-querying for fresh User data. + * + * If your firewall is "stateless: true" (for a pure API), this + * method is not called. + * + * @return UserInterface + */ + public function refreshUser(UserInterface $user) + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user))); + } + + // Return a User object after making sure its data is "fresh". + // Or throw a UsernameNotFoundException if the user no longer exists. + throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__); + } + + /** + * Tells Symfony to use this provider for this User class. + */ + public function supportsClass($class) + { + return User::class === $class; + } + } + +Most of the work is already done! Read the comments in the code and update the +TODO sections to finish the user provider. When you're done, tell Symfony about +the user provider by adding it in ``security.yaml``: + +.. code-block:: yaml + + # config/packages/security.yaml + security: + providers: + # the name of your user provider can be anything + your_custom_user_provider: + id: App\Security\UserProvider + +Lastly, update the ``config/packages/security.yaml`` file to set the +``provider`` key to ``your_custom_user_provider`` in all the firewalls which +will use this custom user provider. + +.. _user_session_refresh: + +Understanding how Users are Refreshed from the Session +------------------------------------------------------ + +At the end of every request (unless your firewall is ``stateless``), your +``User`` object is serialized to the session. At the beginning of the next +request, it's deserialized and then passed to your user provider to "refresh" it +(e.g. Doctrine queries for a fresh user). + +Then, the two User objects (the original from the session and the refreshed User +object) are "compared" to see if they are "equal". By default, the core +``AbstractToken`` class compares the return values of the ``getPassword()``, +``getSalt()`` and ``getUsername()`` methods. If any of these are different, your +user will be logged out. This is a security measure to make sure that malicious +users can be de-authenticated if core user data changes. + +However, in some cases, this process can cause unexpected authentication problems. +If you're having problems authenticating, it could be that you *are* authenticating +successfully, but you immediately lose authentication after the first redirect. + +In that case, review the serialization logic (e.g. ``SerializableInterface``) if +you have any, to make sure that all the fields necessary are serialized. + +Comparing Users Manually with EquatableInterface +------------------------------------------------ + +Or, if you need more control over the "compare users" process, make your User class +implement :class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface`. +Then, your ``isEqualTo()`` method will be called when comparing users. + +Injecting a User Provider in your Services +------------------------------------------ + +Symfony defines several services related to user providers: + +.. code-block:: terminal + + $ php bin/console debug:container user.provider + + Select one of the following services to display its information: + [0] security.user.provider.in_memory + [1] security.user.provider.ldap + [2] security.user.provider.chain + ... + +Most of these services are abstract and cannot be injected in your services. +Instead, you must inject the normal service that Symfony creates for each of +your user providers. The names of these services follow this pattern: +``security.user.provider.concrete.``. + +For example, if you are :doc:`building a form login ` +and want to inject in your ``LoginFormAuthenticator`` a user provider of type +``memory`` and called ``backend_users``, do the following:: + + // src/Security/LoginFormAuthenticator.php + namespace App\Security; + + use Symfony\Component\Security\Core\User\InMemoryUserProvider; + use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; + + class LoginFormAuthenticator extends AbstractFormLoginAuthenticator + { + private $userProvider; + + // change the 'InMemoryUserProvider' type-hint in the constructor if + // you are injecting a different type of user provider + public function __construct(InMemoryUserProvider $userProvider, /* ... */) + { + $this->userProvider = $userProvider; + // ... + } + } + +Then, inject the concrete service created by Symfony for the ``backend_users`` +user provider: + +.. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\Security\LoginFormAuthenticator: + $userProvider: '@security.user.provider.concrete.backend_users' diff --git a/security/voters.rst b/security/voters.rst index 7d76949f5b6..05415c1f39f 100644 --- a/security/voters.rst +++ b/security/voters.rst @@ -20,7 +20,7 @@ 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 -authorization checker or call ``denyAccessUnlessGranted`` in a controller (which +authorization checker or call ``denyAccessUnlessGranted()`` in a controller (which uses the authorization checker), or by :ref:`access controls `. @@ -57,15 +57,15 @@ Suppose you have a ``Post`` object and you need to decide whether or not the cur user can *edit* or *view* the object. In your controller, you'll check access with code like this:: - // src/AppBundle/Controller/PostController.php + // src/Controller/PostController.php // ... - class PostController extends Controller + class PostController extends AbstractController { /** * @Route("/posts/{id}", name="post_show") */ - public function showAction($id) + public function show($id) { // get a Post object - e.g. query for it $post = ...; @@ -79,7 +79,7 @@ code like this:: /** * @Route("/posts/{id}/edit", name="post_edit") */ - public function editAction($id) + public function edit($id) { // get a Post object - e.g. query for it $post = ...; @@ -104,11 +104,11 @@ pretty complex. For example, a ``User`` can always edit or view a ``Post`` they And if a ``Post`` is marked as "public", anyone can view it. A voter for this situation would look like this:: - // src/AppBundle/Security/PostVoter.php - namespace AppBundle\Security; + // src/Security/PostVoter.php + namespace App\Security; - use AppBundle\Entity\Post; - use AppBundle\Entity\User; + use App\Entity\Post; + use App\Entity\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; @@ -203,7 +203,7 @@ Configuring the Voter To inject the voter into the security layer, you must declare it as a service and tag it with ``security.voter``. But if you're using the -:ref:`default services.yml configuration `, +:ref:`default services.yaml configuration `, that's done automatically for you! When you :ref:`call isGranted() with view/edit and pass a Post object `, your voter will be executed and you can control access. @@ -213,24 +213,24 @@ Checking for Roles inside a Voter What if you want to call ``isGranted()`` from *inside* your voter - e.g. you want to see if the current user has ``ROLE_SUPER_ADMIN``. That's possible by injecting -the :class:`Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager` +the :class:`Symfony\\Component\\Security\\Core\\Security` into your voter. You can use this to, for example, *always* allow access to a user with ``ROLE_SUPER_ADMIN``:: - // src/AppBundle/Security/PostVoter.php + // src/Security/PostVoter.php // ... - use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; + use Symfony\Component\Security\Core\Security; class PostVoter extends Voter { // ... - private $decisionManager; + private $security; - public function __construct(AccessDecisionManagerInterface $decisionManager) + public function __construct(Security $security) { - $this->decisionManager = $decisionManager; + $this->security = $security; } protected function voteOnAttribute($attribute, $subject, TokenInterface $token) @@ -238,7 +238,7 @@ with ``ROLE_SUPER_ADMIN``:: // ... // ROLE_SUPER_ADMIN can do anything! The power! - if ($this->decisionManager->decide($token, ['ROLE_SUPER_ADMIN'])) { + if ($this->security->isGranted('ROLE_SUPER_ADMIN')) { return true; } @@ -246,20 +246,10 @@ with ``ROLE_SUPER_ADMIN``:: } } -If you're using the :ref:`default services.yml configuration `, -you're done! Symfony will automatically pass the ``security.access.decision_manager`` +If you're using the :ref:`default services.yaml configuration `, +you're done! Symfony will automatically pass the ``security.helper`` service when instantiating your voter (thanks to autowiring). -Calling ``decide()`` on the ``AccessDecisionManager`` is essentially the same as -calling ``isGranted()`` from a controller or other places -(it's just a little lower-level, which is necessary for a voter). - -.. note:: - - If you need to check access in any non-voter service, use the ``security.authorization_checker`` - service (i.e. type-hint ``Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface``) - instead of the ``security.access.decision_manager`` service shown here. - .. _security-voters-change-strategy: Changing the Access Decision Strategy @@ -295,7 +285,7 @@ security configuration: .. code-block:: yaml - # app/config/security.yml + # config/packages/security.yaml security: access_decision_manager: strategy: unanimous @@ -303,7 +293,7 @@ security configuration: .. code-block:: xml - + loadFromExtension('security', [ 'access_decision_manager' => [ 'strategy' => 'unanimous', diff --git a/serializer.rst b/serializer.rst index b1e64881831..e6c7eedb82b 100644 --- a/serializer.rst +++ b/serializer.rst @@ -4,80 +4,38 @@ 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 -:doc:`Serializer Component `, which gives you some -tools that you can leverage for your solution. +Symfony provides a serializer to serialize/deserialize to and from objects and +different formats (e.g. JSON or XML). Before using it, read the +:doc:`Serializer component docs ` to get familiar with +its philosophy and the normalizers and encoders terminology. -In fact, before you start, get familiar with the serializer, normalizers -and encoders by reading the :doc:`Serializer Component ` -documentation. +.. _activating_the_serializer: -Activating the Serializer -------------------------- +Installation +------------ -The ``serializer`` service is not available by default. To turn it on, activate -it in your configuration: +In applications using :ref:`Symfony Flex `, run this command to +install the ``serializer`` :ref:`Symfony pack ` before using it: -.. configuration-block:: +.. code-block:: terminal - .. code-block:: yaml - - # app/config/config.yml - framework: - # ... - serializer: { enable_annotations: true } - # Alternatively, if you don't want to use annotations - #serializer: { enabled: true } - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', [ - // ... - 'serializer' => [ - 'enable_annotations' => true, - // Alternatively, if you don't want to use annotations - //'enabled' => true, - ], - ]); + $ composer require symfony/serializer-pack Using the Serializer Service ---------------------------- -Once enabled, the ``serializer`` service can be injected in any service where +Once enabled, the serializer service can be injected in any service where you need it or it can be used in a controller:: - // src/AppBundle/Controller/DefaultController.php - namespace AppBundle\Controller; + // src/Controller/DefaultController.php + namespace App\Controller; - use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Serializer\SerializerInterface; - class DefaultController extends Controller + class DefaultController extends AbstractController { - public function indexAction(SerializerInterface $serializer) + public function index(SerializerInterface $serializer) { // keep reading for usage examples } @@ -103,6 +61,8 @@ As well as the following normalizers: handle typical data objects * :class:`Symfony\\Component\\Serializer\\Normalizer\\DateTimeNormalizer` for objects implementing the :phpclass:`DateTimeInterface` interface +* :class:`Symfony\\Component\\Serializer\\Normalizer\\DateTimeZoneNormalizer` for + :phpclass:`DateTimeZone` objects * :class:`Symfony\\Component\\Serializer\\Normalizer\\DataUriNormalizer` to transform :phpclass:`SplFileInfo` objects in `Data URIs`_ * :class:`Symfony\\Component\\Serializer\\Normalizer\\JsonSerializableNormalizer` @@ -125,7 +85,7 @@ properties and setters (``setXxx()``) to change properties: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: get_set_method_normalizer: class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer @@ -134,7 +94,7 @@ properties and setters (``setXxx()``) to change properties: .. code-block:: xml - + register('get_set_method_normalizer', GetSetMethodNormalizer::class) @@ -158,83 +118,48 @@ properties and setters (``setXxx()``) to change properties: ->addTag('serializer.normalizer') ; -.. versionadded:: 3.4 - - Support for hasser methods (``hasXxx()``) in ``GetSetMethodNormalizer`` was - introduced in Symfony 3.4. In previous Symfony versions only getters (``getXxx()``) - and issers (``isXxx()``) were supported. - .. _serializer-using-serialization-groups-annotations: Using Serialization Groups Annotations -------------------------------------- -Enable :ref:`serialization groups annotation ` -with the following configuration: - -.. configuration-block:: +To use annotations, first add support for them via the SensioFrameworkExtraBundle: - .. code-block:: yaml +.. code-block:: terminal - # app/config/config.yml - framework: - # ... - serializer: - enable_annotations: true - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', [ - // ... - 'serializer' => [ - 'enable_annotations' => true, - ], - ]); + $ composer require sensio/framework-extra-bundle Next, add the :ref:`@Groups annotations ` to your class and choose which groups to use when serializing:: $json = $serializer->serialize( $someObject, - 'json', ['groups' => ['group1']] + 'json', ['groups' => 'group1'] ); +.. tip:: + + The value of the ``groups`` key can be a single string, or an array of strings. + In addition to the ``@Groups`` annotation, the Serializer component also -supports Yaml or XML files. These files are automatically loaded when being +supports YAML or XML files. These files are automatically loaded when being stored in one of the following locations: -* The ``serialization.yml`` or ``serialization.xml`` file in +* All ``*.yaml`` and ``*.xml`` files in the ``config/serializer/`` + directory. +* The ``serialization.yaml`` or ``serialization.xml`` file in the ``Resources/config/`` directory of a bundle; -* All ``*.yml`` and ``*.xml`` files in the ``Resources/config/serialization/`` +* All ``*.yaml`` and ``*.xml`` files in the ``Resources/config/serialization/`` directory of a bundle. .. _serializer-enabling-metadata-cache: -Enabling the Metadata Cache ---------------------------- +Configuring the Metadata Cache +------------------------------ -Metadata used by the Serializer component such as groups can be cached to -enhance application performance. By default, the serializer uses the ``cache.system`` -cache pool which is configured using the :ref:`cache.system ` +The metadata for the serializer is automatically cached to enhance application +performance. By default, the serializer uses the ``cache.system`` cache pool +which is configured using the :ref:`cache.system ` option. Enabling a Name Converter @@ -252,7 +177,7 @@ value: .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: # ... serializer: @@ -260,7 +185,7 @@ value: .. code-block:: xml - + @@ -268,7 +193,7 @@ value: .. code-block:: php - // app/config/config.php + // config/packages/framework.php $container->loadFromExtension('framework', [ // ... 'serializer' => [ @@ -279,8 +204,19 @@ value: Going Further with the Serializer --------------------------------- -`ApiPlatform`_ provides an API system supporting `JSON-LD`_ and `Hydra Core Vocabulary`_ -hypermedia formats. It is built on top of the Symfony Framework and its Serializer +`API Platform`_ provides an API system supporting the following formats: + +* `JSON-LD`_ along with the `Hydra Core Vocabulary`_ +* `OpenAPI`_ v2 (formerly Swagger) and v3 +* `GraphQL`_ +* `JSON:API`_ +* `HAL`_ +* JSON +* XML +* YAML +* CSV + +It is built on top of the Symfony Framework and its Serializer component. It provides custom normalizers and a custom encoder, custom metadata and a caching system. @@ -289,11 +225,16 @@ take a look at how this bundle works. .. toctree:: :maxdepth: 1 - :glob: - serializer/* + serializer/normalizers + serializer/custom_encoders + serializer/custom_normalizer -.. _`ApiPlatform`: https://github.com/api-platform/core +.. _`API Platform`: https://api-platform.com .. _`JSON-LD`: http://json-ld.org .. _`Hydra Core Vocabulary`: http://hydra-cg.com +.. _`OpenAPI`: https://www.openapis.org +.. _`GraphQL`: https://graphql.org +.. _`JSON:API`: https://jsonapi.org +.. _`HAL`: http://stateless.co/hal_specification.html .. _`Data URIs`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs diff --git a/serializer/custom_encoders.rst b/serializer/custom_encoders.rst index b673304e4f2..5bb78def4e4 100644 --- a/serializer/custom_encoders.rst +++ b/serializer/custom_encoders.rst @@ -9,17 +9,17 @@ to transform any data to an array. Then, by leveraging *Encoders*, that data can be converted into any data-structure (e.g. JSON). The Component provides several built-in encoders that are described -:doc:`in their own section ` but you may want +:doc:`in the serializer component ` but you may want to use another structure that's not supported. Creating a new encoder ---------------------- -Imagine you want to serialize and deserialize Yaml. For that you'll have to +Imagine you want to serialize and deserialize YAML. For that you'll have to create your own encoder that uses the :doc:`Yaml Component `:: - namespace AppBundle\Serializer; + namespace App\Serializer; use Symfony\Component\Serializer\Encoder\DecoderInterface; use Symfony\Component\Serializer\Encoder\EncoderInterface; @@ -32,7 +32,7 @@ create your own encoder that uses the return Yaml::dump($data); } - public function supportsEncoding($format, array $context = []) + public function supportsEncoding($format) { return 'yaml' === $format; } @@ -42,22 +42,30 @@ create your own encoder that uses the return Yaml::parse($data); } - public function supportsDecoding($format, array $context = []) + public function supportsDecoding($format) { return 'yaml' === $format; } } +.. tip:: + + If you need access to ``$context`` in your ``supportsDecoding`` or + ``supportsEncoding`` method, make sure to implement + ``Symfony\Component\Serializer\Encoder\ContextAwareDecoderInterface`` + or ``Symfony\Component\Serializer\Encoder\ContextAwareEncoderInterface`` accordingly. + + Registering it in your app -------------------------- If you use the Symfony Framework. then you probably want to register this encoder -as a service in your app. If you're using the :ref:`default services.yml configuration `, +as a service in your app. If you're using the :ref:`default services.yaml configuration `, that's done automatically! .. tip:: - If you're not using autoconfigure, make sure to register your class as a service - and tag it with ``serializer.encoder``. + If you're not using :ref:`autoconfigure `, make sure + to register your class as a service and tag it with ``serializer.encoder``. -Now you'll be able to serialize and deserialize Yaml! +Now you'll be able to serialize and deserialize YAML! diff --git a/serializer/custom_normalizer.rst b/serializer/custom_normalizer.rst index 1794590c5c8..9f2e50fdcf1 100644 --- a/serializer/custom_normalizer.rst +++ b/serializer/custom_normalizer.rst @@ -21,10 +21,10 @@ to customize the normalized data. To do that, leverage the ``ObjectNormalizer``: use App\Entity\Topic; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; - class TopicNormalizer implements NormalizerInterface + class TopicNormalizer implements ContextAwareNormalizerInterface { private $router; private $normalizer; @@ -58,5 +58,5 @@ Registering it in your Application Before using this normalizer in a Symfony application it must be registered as a service and :doc:`tagged ` with ``serializer.normalizer``. -If you're using the :ref:`default services.yml configuration `, +If you're using the :ref:`default services.yaml configuration `, this is done automatically! diff --git a/serializer/encoders.rst b/serializer/encoders.rst deleted file mode 100644 index 5e4dc9f8b53..00000000000 --- a/serializer/encoders.rst +++ /dev/null @@ -1,77 +0,0 @@ -.. index:: - single: Serializer, Encoders - -Encoders -======== - -Encoders basically turn **arrays** into **formats** and vice versa. -They implement -:class:`Symfony\\Component\\Serializer\\Encoder\\EncoderInterface` for -encoding (array to format) and -:class:`Symfony\\Component\\Serializer\\Encoder\\DecoderInterface` for -decoding (format to array). - -You can add new encoders to a Serializer instance by using its second constructor argument:: - - use Symfony\Component\Serializer\Encoder\JsonEncoder; - use Symfony\Component\Serializer\Encoder\XmlEncoder; - use Symfony\Component\Serializer\Serializer; - - $encoders = [new XmlEncoder(), new JsonEncoder()]; - $serializer = new Serializer([], $encoders); - -Built-in Encoders ------------------ - -The Serializer component provides built-in encoders: - -* :class:`Symfony\\Component\\Serializer\\Encoder\\CsvEncoder` to encode/decode CSV -* :class:`Symfony\\Component\\Serializer\\Encoder\\JsonEncoder` to encode/decode JSON -* :class:`Symfony\\Component\\Serializer\\Encoder\\XmlEncoder` to encode/decode XML -* :class:`Symfony\\Component\\Serializer\\Encoder\\YamlEncoder` to encode/decode Yaml - -.. versionadded:: 3.2 - - The :class:`Symfony\\Component\\Serializer\\Encoder\\CsvEncoder` and the - :class:`Symfony\\Component\\Serializer\\Encoder\\YamlEncoder` were introduced in - Symfony 3.2. - -The ``JsonEncoder`` -~~~~~~~~~~~~~~~~~~~ - -The ``JsonEncoder`` encodes to and decodes from JSON strings, based on the PHP -:phpfunction:`json_encode` and :phpfunction:`json_decode` functions. - -The ``XmlEncoder`` -~~~~~~~~~~~~~~~~~~ - -This encoder transforms arrays into XML and vice versa. - -For example, take an object normalized as following:: - - ['foo' => [1, 2], 'bar' => true]; - -The ``XmlEncoder`` will encode this object like that:: - - - - 1 - 2 - 1 - - -Be aware that this encoder will consider keys beginning with ``@`` as attributes:: - - $encoder = new XmlEncoder(); - $encoder->encode(['foo' => ['@bar' => 'value']]); - // will return: - // - // - // - // - -The ``YamlEncoder`` -~~~~~~~~~~~~~~~~~~~ - -This encoder requires the :doc:`Yaml Component ` and -transforms from and to Yaml. diff --git a/service_container.rst b/service_container.rst index 0513559aaf9..5eefce21d5d 100644 --- a/service_container.rst +++ b/service_container.rst @@ -15,15 +15,10 @@ send emails while another object might help you save things to the database. Almost *everything* that your app "does" is actually done by one of these objects. And each time you install a new bundle, you get access to even more! -In Symfony, these useful objects are called **services** and each service lives inside -a very special object called the **service container**. If you have the service container, -then you can fetch a service by using that service's id:: - - $logger = $container->get('logger'); - $entityManager = $container->get('doctrine.orm.entity_manager'); - -The container allows you to centralize the way objects are constructed. It makes -your life easier, promotes a strong architecture and is super fast! +In Symfony, these useful objects are called **services** and each service lives +inside a very special object called the **service container**. The container +allows you to centralize the way objects are constructed. It makes your life +easier, promotes a strong architecture and is super fast! Fetching and using Services --------------------------- @@ -33,87 +28,61 @@ These are like *tools*: waiting for you to take advantage of them. In your contr you can "ask" for a service from the container by type-hinting an argument with the service's class or interface name. Want to :doc:`log ` something? No problem:: - // src/AppBundle/Controller/ProductController.php - // ... + // src/Controller/ProductController.php + namespace App\Controller; use Psr\Log\LoggerInterface; - /** - * @Route("/products") - */ - public function listAction(LoggerInterface $logger) + class ProductController { - $logger->info('Look! I just used a service'); + /** + * @Route("/products") + */ + public function list(LoggerInterface $logger) + { + $logger->info('Look! I just used a service'); - // ... + // ... + } } -.. versionadded:: 3.3 - - The ability to type-hint a service in order to receive it was introduced in Symfony 3.3. - See the :ref:`controller chapter ` for more - details. - -.. _container-debug-container: What other services are available? Find out by running: .. code-block:: terminal - $ php bin/console debug:container + $ php bin/console debug:autowiring - # this is just a *small* sample of the output... - =============================== ================================================================== - Service ID Class name - =============================== ================================================================== - doctrine Doctrine\Bundle\DoctrineBundle\Registry - filesystem Symfony\Component\Filesystem\Filesystem - form.factory Symfony\Component\Form\FormFactory - logger Symfony\Bridge\Monolog\Logger - request_stack Symfony\Component\HttpFoundation\RequestStack - router Symfony\Bundle\FrameworkBundle\Routing\Router - security.authorization_checker Symfony\Component\Security\Core\Authorization\AuthorizationChecker - security.password_encoder Symfony\Component\Security\Core\Encoder\UserPasswordEncoder - session Symfony\Component\HttpFoundation\Session\Session - translator Symfony\Component\Translation\DataCollectorTranslator - twig Twig\Environment - validator Symfony\Component\Validator\Validator\ValidatorInterface - =============================== ================================================================== - -You can also use the unique "Service ID" to access a service directly:: - - // src/AppBundle/Controller/ProductController.php - namespace AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\Routing\Annotation\Route; - - class ProductController extends Controller - { - /** - * @Route("/products") - */ - public function listAction() - { - $logger = $this->container->get('logger'); - $logger->info('Look! I just used a service'); + # this is just a *small* sample of the output... - // ... - } - } + Describes a logger instance. + Psr\Log\LoggerInterface (monolog.logger) -:ref:`Fetching a service directly from the container ` -like this only works if you extend the ``Controller`` class. + Request stack that controls the lifecycle of requests. + Symfony\Component\HttpFoundation\RequestStack (request_stack) + + Interface for the session. + Symfony\Component\HttpFoundation\Session\SessionInterface (session) + + RouterInterface is the interface that all Router classes must implement. + Symfony\Component\Routing\RouterInterface (router.default) + + [...] + +When you use these type-hints in your controller methods or inside your +:ref:`own services `, Symfony will automatically +pass you the service object matching that type. Throughout the docs, you'll see how to use the many different services that live in the container. -.. sidebar:: Container: Lazy-loaded for speed +.. tip:: - Wait! Are all the services (objects) instantiated on *every* request? No! The - container is lazy: it doesn't instantiate a service until (and unless) you ask - for it. For example, if you never use the ``validator`` service during a request, - the container will never instantiate it. + There are actually *many* more services in the container, and each service has + a unique id in the container, like ``session`` or ``router.default``. For a full + list, you can run ``php bin/console debug:container``. But most of the time, + you won't need to worry about this. See :ref:`services-wire-specific-service`. + See :doc:`/service_container/debug`. .. index:: single: Service Container; Configuring services @@ -123,17 +92,12 @@ in the container. Creating/Configuring Services in the Container ---------------------------------------------- -.. tip:: - - The recommended way of configuring services changed in Symfony 3.3. For a deep - explanation, see :doc:`/service_container/3.3-di-changes`. - You can also organize your *own* code into services. For example, suppose you need to show your users a random, happy message. If you put this code in your controller, it can't be re-used. Instead, you decide to create a new class:: - // src/AppBundle/Service/MessageGenerator.php - namespace AppBundle\Service; + // src/Service/MessageGenerator.php + namespace App\Service; class MessageGenerator { @@ -154,9 +118,9 @@ it can't be re-used. Instead, you decide to create a new class:: Congratulations! You've just created your first service class! You can use it immediately inside your controller:: - use AppBundle\Service\MessageGenerator; + use App\Service\MessageGenerator; - public function newAction(MessageGenerator $messageGenerator) + public function new(MessageGenerator $messageGenerator) { // thanks to the type-hint, the container will instantiate a // new MessageGenerator and pass it to you! @@ -175,34 +139,36 @@ each time you ask for it. .. _service-container-services-load-example: -.. sidebar:: Automatic Service Loading in ``services.yml`` +.. sidebar:: Automatic Service Loading in services.yaml - The documentation assumes you're using - `Symfony Standard Edition (version 3.3) services.yml`_ configuration. The most - important part is this: + The documentation assumes you're using the following service configuration, + which is the default config for a new project: .. configuration-block:: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # default configuration for services in *this* file _defaults: - autowire: true - autoconfigure: true - public: false + autowire: true # Automatically injects dependencies in your services. + autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + public: false # Allows optimizing the container by removing unused services; this also means + # fetching services directly from the container via $container->get() won't work. + # The best practice is to be explicit about your dependencies anyway. + + # makes classes in src/ available to be used as services + # this creates a service per class whose id is the fully-qualified class name + App\: + resource: '../src/*' + exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' - # makes classes in src/AppBundle available to be used as services - AppBundle\: - resource: '../../src/AppBundle/*' - # you can exclude directories or files - # but if a service is unused, it's removed anyway - exclude: '../../src/AppBundle/{Entity,Repository}' + # ... .. code-block:: xml - + - - + .. code-block:: php - // app/config/services.php - use Symfony\Component\DependencyInjection\Definition; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - // To use as default template - $definition = new Definition(); + return function(ContainerConfigurator $configurator) { + // default configuration for services in *this* file + $services = $configurator->services() + ->defaults() + ->autowire() // Automatically injects dependencies in your services. + ->autoconfigure() // Automatically registers your services as commands, event subscribers, etc. + ; - $definition - ->setAutowired(true) - ->setAutoconfigured(true) - ->setPublic(false) - ; - - // $this is a reference to the current loader - $this->registerClasses($definition, 'AppBundle\\', '../../src/AppBundle/*', '../../src/AppBundle/{Entity,Repository}'); + // makes classes in src/ available to be used as services + // this creates a service per class whose id is the fully-qualified class name + $services->load('App\\', '../src/*') + ->exclude('../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'); + }; .. tip:: The value of the ``resource`` and ``exclude`` options can be any valid - `glob pattern`_. + `glob pattern`_. The value of the ``exclude`` option can also be an + array of glob patterns. Thanks to this configuration, you can automatically use any classes from the - ``src/AppBundle`` directory as a service, without needing to manually configure + ``src/`` directory as a service, without needing to manually configure it. Later, you'll learn more about this in :ref:`service-psr4-loader`. If you'd prefer to manually wire your service, that's totally possible: see :ref:`services-explicitly-configure-wire-services`. - .. versionadded:: 3.3 - - The ``_defaults`` key *and* ability to load services from a directory were added - in Symfony 3.3. - -You can also fetch a service directly from the container via its "id", which will -be its class name in this case:: - - use AppBundle\Service\MessageGenerator; - - // accessing services like this only works if you extend Controller - class ProductController extends Controller - { - public function newAction() - { - // only works if your service is public - $messageGenerator = $this->get(MessageGenerator::class); - - $message = $messageGenerator->getHappyMessage(); - $this->addFlash('success', $message); - // ... - } - } - -However, this only works if you make your service :ref:`public `. - -.. caution:: - - Service ids are case-insensitive (e.g. ``AppBundle\Service\MessageGenerator`` - and ``appbundle\service\messagegenerator`` refer to the same service). But this - was deprecated in Symfony 3.3. Starting in 4.0, service ids will be case sensitive. - .. _services-constructor-injection: Injecting Services/Config into a Service ---------------------------------------- What if you need to access the ``logger`` service from within ``MessageGenerator``? -Your service does *not* have access to the container directly, so you can't fetch -it via ``$this->container->get()``. - -No problem! Instead, create a ``__construct()`` method with a ``$logger`` argument -that has the ``LoggerInterface`` type-hint. Set this on a new ``$logger`` property +No problem! Create a ``__construct()`` method with a ``$logger`` argument that has +the ``LoggerInterface`` type-hint. Set this on a new ``$logger`` property and use it later:: - // src/AppBundle/Service/MessageGenerator.php - // ... + // src/Service/MessageGenerator.php + namespace App\Service; use Psr\Log\LoggerInterface; @@ -317,10 +250,13 @@ That's it! The container will *automatically* know to pass the ``logger`` servic when instantiating the ``MessageGenerator``. How does it know to do this? :ref:`Autowiring `. The key is the ``LoggerInterface`` type-hint in your ``__construct()`` method and the ``autowire: true`` config in -``services.yml``. When you type-hint an argument, the container will automatically +``services.yaml``. When you type-hint an argument, the container will automatically find the matching service. If it can't, you'll see a clear exception with a helpful suggestion. +By the way, this method of adding dependencies to your ``__construct()`` method is +called *dependency injection*. It's a scary term for a simple concept. + .. _services-debug-container-types: How should you know to use ``LoggerInterface`` for the type-hint? You can either @@ -331,18 +267,21 @@ type-hints by running: $ php bin/console debug:autowiring -This is just a small subset of the output: + # this is just a *small* sample of the output... + + Describes a logger instance. + Psr\Log\LoggerInterface (monolog.logger) + + Request stack that controls the lifecycle of requests. + Symfony\Component\HttpFoundation\RequestStack (request_stack) -=============================================================== ===================================== -Service ID Class name -=============================================================== ===================================== -``Psr\Cache\CacheItemPoolInterface`` alias for ``cache.app.recorder`` -``Psr\Log\LoggerInterface`` alias for ``monolog.logger`` -``Symfony\Component\EventDispatcher\EventDispatcherInterface`` alias for ``debug.event_dispatcher`` -``Symfony\Component\HttpFoundation\RequestStack`` alias for ``request_stack`` -``Symfony\Component\HttpFoundation\Session\SessionInterface`` alias for ``session`` -``Symfony\Component\Routing\RouterInterface`` alias for ``router.default`` -=============================================================== ===================================== + Interface for the session. + Symfony\Component\HttpFoundation\Session\SessionInterface (session) + + RouterInterface is the interface that all Router classes must implement. + Symfony\Component\Routing\RouterInterface (router.default) + + [...] Handling Multiple Services ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -350,10 +289,10 @@ Handling Multiple Services Suppose you also want to email a site administrator each time a site update is made. To do that, you create a new class:: - // src/AppBundle/Updates/SiteUpdateManager.php - namespace AppBundle\Updates; + // src/Updates/SiteUpdateManager.php + namespace App\Updates; - use AppBundle\Service\MessageGenerator; + use App\Service\MessageGenerator; class SiteUpdateManager { @@ -381,13 +320,16 @@ made. To do that, you create a new class:: } } -This uses the ``MessageGenerator`` *and* the ``Swift_Mailer`` service. As long as -you're :ref:`loading all services from src/AppBundle `, -you can use the service immediately:: +This needs the ``MessageGenerator`` *and* the ``Swift_Mailer`` service. That's no +problem! In fact, this new service is ready to be used. In a controller, for example, +you can type-hint the new ``SiteUpdateManager`` class and use it:: + + // src/Controller/SiteController.php - use AppBundle\Updates\SiteUpdateManager; + // ... + use App\Updates\SiteUpdateManager; - public function newAction(SiteUpdateManager $siteUpdateManager) + public function new(SiteUpdateManager $siteUpdateManager) { // ... @@ -412,7 +354,7 @@ example, suppose you want to make the admin email configurable: .. code-block:: diff - // src/AppBundle/Updates/SiteUpdateManager.php + // src/Updates/SiteUpdateManager.php // ... class SiteUpdateManager @@ -443,9 +385,7 @@ example, suppose you want to make the admin email configurable: If you make this change and refresh, you'll see an error: -.. code-block:: text - - Cannot autowire service "AppBundle\Updates\SiteUpdateManager": argument "$adminEmail" + Cannot autowire service "App\Updates\SiteUpdateManager": argument "$adminEmail" of method "__construct()" must have a type-hint or be given a value explicitly. That makes sense! There is no way that the container knows what value you want to @@ -455,23 +395,23 @@ pass here. No problem! In your configuration, you can explicitly set this argume .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # ... # same as before - AppBundle\: - resource: '../../src/AppBundle/*' - exclude: '../../src/AppBundle/{Entity,Repository}' + App\: + resource: '../src/*' + exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' # explicitly configure the service - AppBundle\Updates\SiteUpdateManager: + App\Updates\SiteUpdateManager: arguments: $adminEmail: 'manager@example.com' .. code-block:: xml - + - + + - + manager@example.com @@ -493,34 +434,27 @@ pass here. No problem! In your configuration, you can explicitly set this argume .. code-block:: php - // app/config/services.php - use AppBundle\Updates\SiteUpdateManager; - use Symfony\Component\DependencyInjection\Definition; - - // Same as before - $definition = new Definition(); + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - $definition - ->setAutowired(true) - ->setAutoconfigured(true) - ->setPublic(false) - ; + use App\Updates\SiteUpdateManager; - $this->registerClasses($definition, 'AppBundle\\', '../../src/AppBundle/*', '../../src/AppBundle/{Entity,Repository}'); + return function(ContainerConfigurator $configurator) { + // ... - // Explicitly configure the service - $container->getDefinition(SiteUpdateManager::class) - ->setArgument('$adminEmail', 'manager@example.com'); + // same as before + $services->load('App\\', '../src/*') + ->exclude('../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'); -.. versionadded:: 3.3 + $services->set(SiteUpdateManager::class) + ->arg('$adminEmail', 'manager@example.com') + ; + }; - The ability to configure an argument by its name (``$adminEmail``) was introduced - in Symfony 3.3. Previously, you could configure it only by its index (``2`` in - this case) or by using empty quotes for the other arguments. -Thanks to this, the container will pass ``manager@example.com`` as the third argument -to ``__construct`` when creating the ``SiteUpdateManager`` service. The other arguments -will still be autowired. +Thanks to this, the container will pass ``manager@example.com`` to the ``$adminEmail`` +argument of ``__construct`` when creating the ``SiteUpdateManager`` service. The +other arguments will still be autowired. But, isn't this fragile? Fortunately, no! If you rename the ``$adminEmail`` argument to something else - e.g. ``$mainEmail`` - you will get a clear exception when you @@ -532,88 +466,85 @@ Service Parameters ------------------ In addition to holding service objects, the container also holds configuration, -called ``parameters``. To create a parameter, add it under the ``parameters`` key -and reference it with the ``%parameter_name%`` syntax: +called **parameters**. The main article about Symfony configuration explains the +:ref:`configuration parameters ` in detail and shows +all their types (string, boolean, array, binary and PHP constant parameters). + +However, there is another type of parameter related to services. In YAML config, +any string which starts with ``@`` is considered as the ID of a service, instead +of a regular string. In XML config, use the ``type="service"`` type for the +parameter and in PHP config use the ``Reference`` class: .. configuration-block:: .. code-block:: yaml - # app/config/services.yml - parameters: - admin_email: manager@example.com - + # config/services.yaml services: - # ... + App\Service\MessageGenerator: + # this is not a string, but a reference to a service called 'logger' + arguments: ['@logger'] - AppBundle\Updates\SiteUpdateManager: - arguments: - $adminEmail: '%admin_email%' + # if the value of a string parameter starts with '@', you need to escape + # it by adding another '@' so Symfony doesn't consider it a service + # (this will be parsed as the string '@securepassword') + mailer_password: '@@securepassword' .. code-block:: xml - + - - manager@example.com - - - - - - %admin_email% + + .. code-block:: php - // app/config/services.php - use AppBundle\Updates\SiteUpdateManager; - $container->setParameter('admin_email', 'manager@example.com'); + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - $container->autowire(SiteUpdateManager::class) - // ... - ->setArgument('$adminEmail', '%admin_email%'); + use App\Service\MessageGenerator; -Actually, once you define a parameter, it can be referenced via the ``%parameter_name%`` -syntax in *any* other service configuration file - like ``config.yml``. Many parameters -are defined in a :ref:`parameters.yml file `. + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); -You can then fetch the parameter in the service:: + $services->set(MessageGenerator::class) + ->args([ref('logger')]) + ; + }; - class SiteUpdateManager - { - // ... +Working with container parameters is straightforward using the container's +accessor methods for parameters:: - private $adminEmail; + // checks if a parameter is defined (parameter names are case-sensitive) + $container->hasParameter('mailer.transport'); - public function __construct($adminEmail) - { - $this->adminEmail = $adminEmail; - } - } + // gets value of a parameter + $container->getParameter('mailer.transport'); -You can also fetch parameters directly from the container:: + // adds a new parameter + $container->setParameter('mailer.transport', 'sendmail'); - public function newAction() - { - // ... +.. caution:: - // this ONLY works if you extend Controller - $adminEmail = $this->container->getParameter('admin_email'); + The used ``.`` notation is a + :ref:`Symfony convention ` to make parameters + easier to read. Parameters are flat key-value elements, they can't + be organized into a nested array - // or a shorter way! - // $adminEmail = $this->getParameter('admin_email'); - } +.. note:: -For more info about parameters, see :doc:`/service_container/parameters`. + You can only set a parameter before the container is compiled, not at run-time. + To learn more about compiling the container see + :doc:`/components/dependency_injection/compilation`. .. _services-wire-specific-service: @@ -622,8 +553,8 @@ Choose a Specific Service The ``MessageGenerator`` service created earlier requires a ``LoggerInterface`` argument:: - // src/AppBundle/Service/MessageGenerator.php - // ... + // src/Service/MessageGenerator.php + namespace App\Service; use Psr\Log\LoggerInterface; @@ -650,18 +581,21 @@ But, you can control this and pass in a different logger: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # ... same code as before # explicitly configure the service - AppBundle\Service\MessageGenerator: + App\Service\MessageGenerator: arguments: + # the '@' symbol is important: that's what tells the container + # you want to pass the *service* whose id is 'monolog.logger.request', + # and not just the *string* 'monolog.logger.request' $logger: '@monolog.logger.request' .. code-block:: xml - + - + @@ -680,23 +614,30 @@ But, you can control this and pass in a different logger: .. code-block:: php - // app/config/services.php - use AppBundle\Service\MessageGenerator; - use Symfony\Component\DependencyInjection\Reference; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Service\MessageGenerator; + + return function(ContainerConfigurator $configurator) { + // ... same code as before - $container->autowire(MessageGenerator::class) - ->setAutoconfigured(true) - ->setPublic(false) - ->setArgument('$logger', new Reference('monolog.logger.request')); + // explicitly configure the service + $services->set(SiteUpdateManager::class) + ->arg('$logger', ref('monolog.logger.request')) + ; + }; This tells the container that the ``$logger`` argument to ``__construct`` should use service whose id is ``monolog.logger.request``. -.. tip:: +.. _container-debug-container: + +For a full list of *all* possible services in the container, run: - The ``@`` symbol is important: that's what tells the container you want to pass - the *service* whose id is ``monolog.logger.request``, and not just the *string* - ``monolog.logger.request``. +.. code-block:: terminal + + $ php bin/console debug:container .. _services-binding: @@ -725,6 +666,11 @@ You can also use the ``bind`` keyword to bind specific arguments by name or type # service that's defined in this file Psr\Log\LoggerInterface: '@monolog.logger.request' + # optionally you can define both the name and type of the argument to match + string $adminEmail: 'manager@example.com' + Psr\Log\LoggerInterface $requestLogger: '@monolog.logger.request' + iterable $rules: !tagged_iterator app.foo.rule + # ... .. code-block:: xml @@ -747,6 +693,17 @@ You can also use the ``bind`` keyword to bind specific arguments by name or type type="service" id="monolog.logger.request" /> + + + manager@example.com + + @@ -756,22 +713,40 @@ You can also use the ``bind`` keyword to bind specific arguments by name or type .. code-block:: php // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use App\Controller\LuckyController; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Reference; - $container->register(LuckyController::class) - ->setPublic(true) - ->setBindings([ - '$adminEmail' => 'manager@example.com', - '$requestLogger' => new Reference('monolog.logger.request'), - LoggerInterface::class => new Reference('monolog.logger.request'), - ]) - ; + return function(ContainerConfigurator $configurator) { + $services = $configurator->services() + ->defaults() + // pass this value to any $adminEmail argument for any service + // that's defined in this file (including controller arguments) + ->bind('$adminEmail', 'manager@example.com') + + // pass this service to any $requestLogger argument for any + // service that's defined in this file + ->bind('$requestLogger', ref('monolog.logger.request')) + + // pass this service for any LoggerInterface type-hint for any + // service that's defined in this file + ->bind(LoggerInterface::class, ref('monolog.logger.request')) + + // optionally you can define both the name and type of the argument to match + ->bind('string $adminEmail', 'manager@example.com') + ->bind(LoggerInterface::class.' $requestLogger', ref('monolog.logger.request')) + ->bind('iterable $rules', tagged_iterator('app.foo.rule')) + ; + + // ... + }; By putting the ``bind`` key under ``_defaults``, you can specify the value of *any* argument for *any* service defined in this file! You can bind arguments by name -(e.g. ``$adminEmail``) or by type (e.g. ``Psr\Log\LoggerInterface``). +(e.g. ``$adminEmail``), by type (e.g. ``Psr\Log\LoggerInterface``) or both +(e.g. ``Psr\Log\LoggerInterface $requestLogger``). The ``bind`` config can also be applied to specific services or when loading many services at once (i.e. :ref:`service-psr4-loader`). @@ -781,7 +756,7 @@ services at once (i.e. :ref:`service-psr4-loader`). The autowire Option ------------------- -Above, the ``services.yml`` file has ``autowire: true`` in the ``_defaults`` section +Above, the ``services.yaml`` file has ``autowire: true`` in the ``_defaults`` section so that it applies to all services defined in that file. With this setting, you're able to type-hint arguments in the ``__construct()`` method of your services and the container will automatically pass you the correct arguments. This entire entry @@ -794,130 +769,84 @@ For more details about autowiring, check out :doc:`/service_container/autowiring The autoconfigure Option ------------------------ -.. versionadded:: 3.3 - - The ``autoconfigure`` option was introduced in Symfony 3.3. - -Above, the ``services.yml`` file has ``autoconfigure: true`` in the ``_defaults`` +Above, the ``services.yaml`` file has ``autoconfigure: true`` in the ``_defaults`` section so that it applies to all services defined in that file. With this setting, the container will automatically apply certain configuration to your services, based on your service's *class*. This is mostly used to *auto-tag* your services. -For example, to create a Twig Extension, you need to create a class, register it -as a service, and :doc:`tag ` it with ``twig.extension``: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/services.yml - services: - # ... - - AppBundle\Twig\MyTwigExtension: - tags: [twig.extension] - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/services.php - use AppBundle\Twig\MyTwigExtension; - - $container->autowire(MyTwigExtension::class) - ->addTag('twig.extension'); +For example, to create a Twig extension, you need to create a class, register it +as a service, and :doc:`tag ` it with ``twig.extension``. But, with ``autoconfigure: true``, you don't need the tag. In fact, if you're using -the :ref:`Symfony Standard Edition services.yml config `, +the :ref:`default services.yaml config `, you don't need to do *anything*: the service will be automatically loaded. Then, ``autoconfigure`` will add the ``twig.extension`` tag *for* you, because your class implements ``Twig\Extension\ExtensionInterface``. And thanks to ``autowire``, you can even add constructor arguments without any configuration. -You can still :ref:`manually configure the service ` -if you need to. - -.. _container-public: - -Public Versus Private Services ------------------------------- +Linting Service Definitions +--------------------------- -Thanks to the ``_defaults`` section in ``services.yml``, every service defined in -this file is ``public: false`` by default: +The ``lint:container`` command checks that the arguments injected into services +match their type declarations. It's useful to run it before deploying your +application to production (e.g. in your continuous integration server): -.. configuration-block:: +.. code-block:: terminal - .. code-block:: yaml + $ php bin/console lint:container - # app/config/services.yml - services: - # default configuration for services in *this* file - _defaults: - # ... - public: false +Checking the types of all service arguments whenever the container is compiled +can hurt performance. That's why this type checking is implemented in a +:doc:`compiler pass ` called +``CheckTypeDeclarationsPass`` which is disabled by default and enabled only when +executing the ``lint:container`` command. If you don't mind the performance +loss, enable the compiler pass in your application. - .. code-block:: xml +.. _container-public: - - - +Public Versus Private Services +------------------------------ - - - - - +Thanks to the ``_defaults`` section in ``services.yaml``, every service defined in +this file is ``public: false`` by default. -What does this mean? When a service is **not** public, you cannot access it directly -from the container:: +What does this mean? When a service **is** public, you can access it directly +from the container object, which is accessible from any controller that extends +:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController`:: - use AppBundle\Service\MessageGenerator; + use App\Service\MessageGenerator; - public function newAction(MessageGenerator $messageGenerator) + // ... + public function new() { - // type-hinting it as an argument DOES work + // there IS a public "logger" service in the container + $logger = $this->container->get('logger'); - // but accessing it directly from the container does NOT Work - $this->container->get(MessageGenerator::class); + // this will NOT work: MessageGenerator is a private service + $generator = $this->container->get(MessageGenerator::class); } -Usually, this is OK: there are better ways to access a service. But, if you *do* -need to make your service public, just override this setting: +As a best practice, you should only create *private* services, which will happen +automatically. And also, you should *not* use the ``$container->get()`` method to +fetch public services. + +But, if you *do* need to make a service public, override the ``public`` setting: .. configuration-block:: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # ... same code as before # explicitly configure the service - AppBundle\Service\MessageGenerator: + App\Service\MessageGenerator: public: true .. code-block:: xml - + - + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Service\MessageGenerator; + + return function(ContainerConfigurator $configurator) { + // ... same as code before + + // explicitly configure the service + $services->set(MessageGenerator::class) + ->public() + ; + }; + .. _service-psr4-loader: Importing Many Services at once with resource @@ -944,27 +889,19 @@ key. For example, the default Symfony configuration contains this: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # ... - # the namespace prefix for classes (must end in \) - AppBundle\: - # create services for all the classes found in this directory... - resource: '../../src/AppBundle/*' - # ...except for the classes located in these directories - exclude: '../../src/AppBundle/{Entity,Repository}' - - # these were imported above, but we want to add some extra config - AppBundle\Controller\: - resource: '../../src/AppBundle/Controller' - # apply some configuration to these services - public: true - tags: ['controller.service_arguments'] + # makes classes in src/ available to be used as services + # this creates a service per class whose id is the fully-qualified class name + App\: + resource: '../src/*' + exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' .. code-block:: xml - + - - - - - + .. code-block:: php - // app/config/services.php - use Symfony\Component\DependencyInjection\Definition; - - // To use as default template - $definition = new Definition(); - - $definition - ->setAutowired(true) - ->setAutoconfigured(true) - ->setPublic(false) - ; - - $this->registerClasses($definition, 'AppBundle\\', '../../src/AppBundle/*', '../../src/AppBundle/{Entity,Repository}'); + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - // Changes default config - $definition - ->setPublic(true) - ->addTag('controller.service_arguments') - ; + return function(ContainerConfigurator $configurator) { + // ... - // $this is a reference to the current loader - $this->registerClasses($definition, 'AppBundle\\Controller\\', '../../src/AppBundle/Controller/*'); + // makes classes in src/ available to be used as services + // this creates a service per class whose id is the fully-qualified class name + $services->load('App\\', '../src/*') + ->exclude('../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'); + }; .. tip:: @@ -1025,32 +947,28 @@ them will not cause the container to be rebuilt. .. note:: - Wait, does this mean that *every* class in ``src/AppBundle`` is registered as - a service? Even model or entity classes? Actually, no. As long as you have + Wait, does this mean that *every* class in ``src/`` is registered as + a service? Even model classes? Actually, no. As long as you have ``public: false`` under your ``_defaults`` key (or you can add it under the specific import), all the imported services are *private*. Thanks to this, all - classes in ``src/AppBundle`` that are *not* explicitly used as services are - automatically removed from the final container. In reality, the import simply + classes in ``src/`` that are *not* explicitly used as services are + automatically removed from the final container. In reality, the import means that all classes are "available to be *used* as services" without needing to be manually configured. Multiple Service Definitions Using the Same Namespace ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 3.4 - - The ``namespace`` option in the YAML configuration was introduced in Symfony 3.4. - If you define services using the YAML config format, the PHP namespace is used as the key of each configuration, so you can't define different service configs for classes under the same namespace: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: App\Domain\: - resource: '../../src/Domain/*' + resource: '../src/Domain/*' # ... In order to have multiple definitions, add the ``namespace`` option and use any @@ -1058,16 +976,16 @@ unique string as the key of each service config: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: command_handlers: namespace: App\Domain\ - resource: '../../src/Domain/*/CommandHandler' + resource: '../src/Domain/*/CommandHandler' tags: [command_handler] event_subscribers: namespace: App\Domain\ - resource: '../../src/Domain/*/EventSubscriber' + resource: '../src/Domain/*/EventSubscriber' tags: [event_subscriber] .. _services-explicitly-configure-wire-services: @@ -1088,36 +1006,36 @@ admin email. In this case, each needs to have a unique service id: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # ... # this is the service's id site_update_manager.superadmin: - class: AppBundle\Updates\SiteUpdateManager + class: App\Updates\SiteUpdateManager # you CAN still use autowiring: we just want to show what it looks like without autowire: false # manually wire all arguments arguments: - - '@AppBundle\Service\MessageGenerator' + - '@App\Service\MessageGenerator' - '@mailer' - 'superadmin@example.com' site_update_manager.normal_users: - class: AppBundle\Updates\SiteUpdateManager + class: App\Updates\SiteUpdateManager autowire: false arguments: - - '@AppBundle\Service\MessageGenerator' + - '@App\Service\MessageGenerator' - '@mailer' - 'contact@example.com' # Create an alias, so that - by default - if you type-hint SiteUpdateManager, # the site_update_manager.superadmin will be used - AppBundle\Updates\SiteUpdateManager: '@site_update_manager.superadmin' + App\Updates\SiteUpdateManager: '@site_update_manager.superadmin' .. code-block:: xml - + - - + + superadmin@example.com - - + + contact@example.com - + .. code-block:: php - // app/config/services.php - use AppBundle\Service\MessageGenerator; - use AppBundle\Updates\SiteUpdateManager; - use Symfony\Component\DependencyInjection\Reference; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - $container->register('site_update_manager.superadmin', SiteUpdateManager::class) - ->setAutowired(false) - ->setArguments([ - new Reference(MessageGenerator::class), - new Reference('mailer'), - 'superadmin@example.com' - ]); + use App\Service\MessageGenerator; + use App\Updates\SiteUpdateManager; - $container->register('site_update_manager.normal_users', SiteUpdateManager::class) - ->setAutowired(false) - ->setArguments([ - new Reference(MessageGenerator::class), - new Reference('mailer'), - 'contact@example.com' - ]); + return function(ContainerConfigurator $configurator) { + // ... - $container->setAlias(SiteUpdateManager::class, 'site_update_manager.superadmin') + // site_update_manager.superadmin is the service's id + $services->set('site_update_manager.superadmin', SiteUpdateManager::class) + // you CAN still use autowiring: we just want to show what it looks like without + ->autowire(false) + // manually wire all arguments + ->args([ + ref(MessageGenerator::class), + ref('mailer'), + 'superadmin@example.com', + ]); + + $services->set('site_update_manager.normal_users', SiteUpdateManager::class) + ->autowire(false) + ->args([ + ref(MessageGenerator::class), + ref('mailer'), + 'contact@example.com', + ]); + + // Create an alias, so that - by default - if you type-hint SiteUpdateManager, + // the site_update_manager.superadmin will be used + $services->alias(SiteUpdateManager::class, 'site_update_manager.superadmin'); + }; In this case, *two* services are registered: ``site_update_manager.superadmin`` and ``site_update_manager.normal_users``. Thanks to the alias, if you type-hint @@ -1175,7 +1103,7 @@ If you want to pass the second, you'll need to :ref:`manually wire the service < .. caution:: - If you do *not* create the alias and are :ref:`loading all services from src/AppBundle `, + If you do *not* create the alias and are :ref:`loading all services from src/ `, then *three* services have been created (the automatic service + your two services) and the automatically loaded service will be passed - by default - when you type-hint ``SiteUpdateManager``. That's why creating the alias is a good idea. @@ -1189,6 +1117,5 @@ Learn more /service_container/* -.. _`Symfony Standard Edition (version 3.3) services.yml`: https://github.com/symfony/symfony-standard/blob/3.3/app/config/services.yml .. _`glob pattern`: https://en.wikipedia.org/wiki/Glob_(programming) -.. _`Symfony Fundamentals screencast series`: https://symfonycasts.com/screencast/symfony3-fundamentals +.. _`Symfony Fundamentals screencast series`: https://symfonycasts.com/screencast/symfony-fundamentals diff --git a/service_container/3.3-di-changes.rst b/service_container/3.3-di-changes.rst index 3eb90241982..5e689326269 100644 --- a/service_container/3.3-di-changes.rst +++ b/service_container/3.3-di-changes.rst @@ -1,7 +1,7 @@ The Symfony 3.3 DI Container Changes Explained (autowiring, _defaults, etc) =========================================================================== -If you look at the ``services.yml`` file in a new Symfony 3.3 or newer project, you'll +If you look at the ``services.yaml`` file in a new Symfony 3.3 or newer project, you'll notice some big changes: ``_defaults``, ``autowiring``, ``autoconfigure`` and more. These features are designed to *automate* configuration and make development faster, without sacrificing predictability, which is very important! Another goal is to make @@ -24,50 +24,44 @@ need to actually change your configuration files to use them. .. _`service-33-default_definition`: -The new Default ``services.yml`` File -------------------------------------- +The new Default services.yaml File +---------------------------------- -To understand the changes, look at the new default ``services.yml`` file in the -Symfony Standard Edition: +To understand the changes, look at the new default ``services.yaml`` file (this is +what the file looks like in Symfony 4): .. configuration-block:: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # default configuration for services in *this* file _defaults: - # automatically injects dependencies in your services - autowire: true - # automatically registers your services as commands, event subscribers, etc. - autoconfigure: true - # this means you cannot fetch services directly from the container via $container->get() - # if you need to do this, you can override this setting on individual services - public: false - - # makes classes in src/AppBundle available to be used as services - # this creates a service per class whose id is the fully-qualified class name - AppBundle\: - resource: '../../src/AppBundle/*' - # you can exclude directories or files - # but if a service is unused, it's removed anyway - exclude: '../../src/AppBundle/{Entity,Repository}' + autowire: true # Automatically injects dependencies in your services. + autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + public: false # Allows optimizing the container by removing unused services; this also means + # fetching services directly from the container via $container->get() won't work. + # The best practice is to be explicit about your dependencies anyway. - # controllers are imported separately to make sure they're public - # and have a tag that allows actions to type-hint services - AppBundle\Controller\: - resource: '../../src/AppBundle/Controller' + # makes classes in src/ available to be used as services + # this creates a service per class whose id is the fully-qualified class name + App\: + resource: '../src/*' + exclude: '../src/{Entity,Migrations,Tests}' + + # controllers are imported separately to make sure services can be injected + # as action arguments even if you don't extend any base controller class + App\Controller\: + resource: '../src/Controller' tags: ['controller.service_arguments'] - # add more services, or override services that need manual wiring - # AppBundle\Service\ExampleService: - # arguments: - # $someArgument: 'some_value' + # add more service definitions when explicit configuration is needed + # please note that last definitions always *replace* previous ones .. code-block:: xml - + - + - + @@ -87,6 +81,33 @@ Symfony Standard Edition: + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $configurator) { + // default configuration for services in *this* file + $services = $configurator->services() + ->defaults() + ->autowire() // Automatically injects dependencies in your services. + ->autoconfigure() // Automatically registers your services as commands, event subscribers, etc. + ; + + // makes classes in src/ available to be used as services + // this creates a service per class whose id is the fully-qualified class name + $services->load('App\\', '../src/*') + ->exclude('../src/{Entity,Migrations,Tests}'); + + // controllers are imported separately to make sure services can be injected + // as action arguments even if you don't extend any base controller class + $services->load('App\\Controller\\', '../src/Controller') + ->tag('controller.service_arguments'); + + // add more service definitions when explicit configuration is needed + // please note that last definitions always *replace* previous ones + }; + This small bit of configuration contains a paradigm shift of how services are configured in Symfony. @@ -106,21 +127,19 @@ thanks to the following config: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # ... - # makes classes in src/AppBundle available to be used as services + # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name - AppBundle\: - resource: '../../src/AppBundle/*' - # you can exclude directories or files - # but if a service is unused, it's removed anyway - exclude: '../../src/AppBundle/{Entity,Repository}' + App\: + resource: '../src/*' + exclude: '../src/{Entity,Migrations,Tests}' .. code-block:: xml - + - + -This means that every class in ``src/AppBundle/`` is *available* to be used as a + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $configurator) { + // ... + + // makes classes in src/ available to be used as services + // this creates a service per class whose id is the fully-qualified class name + $services->load('App\\', '../src/*') + ->exclude('../src/{Entity,Migrations,Tests}'); + }; + +This means that every class in ``src/`` is *available* to be used as a service. And thanks to the ``_defaults`` section at the top of the file, all of these services are **autowired** and **private** (i.e. ``public: false``). -The service ids are equal to the class name (e.g. ``AppBundle\Service\InvoiceGenerator``). +The service ids are equal to the class name (e.g. ``App\Service\InvoiceGenerator``). And that's another change you'll notice in Symfony 3.3: we recommend that you use the class name as your service id, unless you have :ref:`multiple services for the same class `. @@ -149,7 +182,7 @@ to determine most arguments automatically. But, you can always override the serv and :ref:`manually configure arguments ` or anything else special about your service. - But wait, if I have some model (non-service) classes in my ``src/AppBundle/`` + But wait, if I have some model (non-service) classes in my ``src/`` directory, doesn't this mean that *they* will also be registered as services? Isn't that a problem? @@ -159,7 +192,7 @@ automatically removed from the compiled container. This means that the number of services in your container should be the *same* whether your explicitly configure each service or load them all at once with this method. - OK, but can I exclude some paths that I *know* won't contain services? + Ok, but can I exclude some paths that I *know* won't contain services? Yes! The ``exclude`` key is a glob pattern that can be used to *blacklist* paths that you do *not* want to be included as services. But, since unused services are @@ -181,19 +214,19 @@ service as an argument to another with the following config: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: app.invoice_generator: - class: AppBundle\Service\InvoiceGenerator + class: App\Service\InvoiceGenerator app.invoice_mailer: - class: AppBundle\Service\InvoiceMailer + class: App\Service\InvoiceMailer arguments: - '@app.invoice_generator' .. code-block:: xml - + + class="App\Service\InvoiceGenerator"/> + class="App\Service\InvoiceMailer"> @@ -214,9 +247,9 @@ service as an argument to another with the following config: .. code-block:: php - // app/config/services.php - use AppBundle\Service\InvoiceGenerator; - use AppBundle\Service\InvoiceMailer; + // config/services.php + use App\Service\InvoiceGenerator; + use App\Service\InvoiceMailer; use Symfony\Component\DependencyInjection\Reference; $container->register('app.invoice_generator', InvoiceGenerator::class); @@ -230,7 +263,7 @@ id's were the main way that you configured things. But in Symfony 3.3, thanks to autowiring, all you need to do is type-hint the argument with ``InvoiceGenerator``:: - // src/AppBundle/Service/InvoiceMailer.php + // src/Service/InvoiceMailer.php // ... class InvoiceMailer @@ -247,8 +280,8 @@ argument with ``InvoiceGenerator``:: That's it! Both services are :ref:`automatically registered ` and set to autowire. Without *any* configuration, the container knows to pass the -auto-registered ``AppBundle\Service\InvoiceGenerator`` as the first argument. As -you can see, the *type* of the class - ``AppBundle\Service\InvoiceGenerator`` - is +auto-registered ``App\Service\InvoiceGenerator`` as the first argument. As +you can see, the *type* of the class - ``App\Service\InvoiceGenerator`` - is what's most important, not the id. You request an *instance* of a specific type and the container automatically passes you the correct service. @@ -264,13 +297,13 @@ which should be used for autowiring. For full details on the autowiring logic, s But what if I have a scalar (e.g. string) argument? How does it autowire that? If you have an argument that is *not* an object, it can't be autowired. But that's -OK! Symfony will give you a clear exception (on the next refresh of *any* page) telling +ok! Symfony will give you a clear exception (on the next refresh of *any* page) telling you which argument of which service could not be autowired. To fix it, you can :ref:`manually configure *just* that one argument `. This is the philosophy of autowiring: only configure the parts that you need to. Most configuration is automated. - OK, but autowiring makes your applications less stable. If you change one thing + Ok, but autowiring makes your applications less stable. If you change one thing or make a mistake, unexpected things might happen. Isn't that a problem? Symfony has always valued stability, security and predictability first. Autowiring @@ -283,9 +316,7 @@ was designed with that in mind. Specifically: * The container determines *which* service to pass in an explicit way: it looks for a service whose id matches the type-hint exactly. It does *not* scan all services - looking for objects that have that class/interface (actually, it *does* do this - in Symfony 3.3, but has been deprecated. If you rely on this, you will see a clear - deprecation warning). + looking for objects that have that class/interface. Autowiring aims to *automate* configuration without magic. @@ -298,19 +329,19 @@ The third big change is that, in a new Symfony 3.3 project, your controllers are .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # ... # controllers are imported separately to make sure they're public # and have a tag that allows actions to type-hint services - AppBundle\Controller\: - resource: '../../src/AppBundle/Controller' + App\Controller\: + resource: '../src/Controller' tags: ['controller.service_arguments'] .. code-block:: xml - + - + @@ -328,17 +359,23 @@ The third big change is that, in a new Symfony 3.3 project, your controllers are .. code-block:: php - // app/config/services.php + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - // ... + return function(ContainerConfigurator $configurator) { + // ... + + // controllers are imported separately to make sure they're public + // and have a tag that allows actions to type-hint services + $services->load('App\\Controller\\', '../src/Controller') + ->tag('controller.service_arguments'); + }; - $definition->addTag('controller.service_arguments'); - $this->registerClasses($definition, 'AppBundle\\Controller\\', '../../src/AppBundle/Controller/*'); But, you might not even notice this. First, your controllers *can* still extend -the same base ``Controller`` class or a new :ref:`AbstractController `. +the same base controller class (``AbstractController``). This means you have access to all of the same shortcuts as before. Additionally, -the ``@Route`` annotation and ``_controller`` syntax (e.g. ``AppBundle:Default:homepage``) +the ``@Route`` annotation and ``_controller`` syntax (e.g. ``App:Default:homepage``) used in routing will automatically use your controller as a service (as long as its service id matches its class name, which it *does* in this case). See :doc:`/controller/service` for more details. You can even create :ref:`invokable controllers ` @@ -353,9 +390,9 @@ action methods, just like you can with the constructor of services. For example: use Psr\Log\LoggerInterface; - class InvoiceController extends Controller + class InvoiceController extends AbstractController { - public function listAction(LoggerInterface $logger) + public function listInvoices(LoggerInterface $logger) { $logger->info('A new way to access services!'); } @@ -369,6 +406,8 @@ In general, the new best practice is to use normal constructor dependency inject (or "action" injection in controllers) instead of fetching public services via ``$this->get()`` (though that does still work). +.. _service_autoconfigure: + 4) Auto-tagging with autoconfigure ---------------------------------- @@ -377,16 +416,16 @@ The fourth big change is the ``autoconfigure`` key, which is set to ``true`` und this file. For example, suppose you want to create an event subscriber. First, you create the class:: - // src/AppBundle/EventSubscriber/SetHeaderSusbcriber.php + // src/EventSubscriber/SetHeaderSusbcriber.php // ... use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\FilterResponseEvent; + use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; class SetHeaderSusbcriber implements EventSubscriberInterface { - public function onKernelResponse(FilterResponseEvent $event) + public function onKernelResponse(ResponseEvent $event) { $event->getResponse()->headers->set('X-SYMFONY-3.3', 'Less config'); } @@ -400,8 +439,10 @@ create the class:: } Great! In Symfony 3.2 or lower, you would now need to register this as a service -in ``services.yml`` and tag it with ``kernel.event_subscriber``. In Symfony 3.3, -you're already done! The service is :ref:`automatically registered `. +in ``services.yaml`` and tag it with ``kernel.event_subscriber``. In Symfony 3.3, +you're already done! + +The service is :ref:`automatically registered `. And thanks to ``autoconfigure``, Symfony automatically tags the service because it implements ``EventSubscriberInterface``. @@ -410,9 +451,9 @@ it implements ``EventSubscriberInterface``. In this case, you've created a class that implements ``EventSubscriberInterface`` and registered it as a service. This is more than enough for the container to know that you want this to be used as an event subscriber: more configuration is not needed. -And the tags system is its own, Symfony-specific mechanism. You can always set -``autoconfigure`` to ``false`` in ``services.yml``, or disable it for a specific -service. +And the tags system is its own, Symfony-specific mechanism. And you can +always set ``autoconfigure`` to ``false`` in ``services.yaml``, or disable it +for a specific service. Does this mean tags are dead? Does this work for all tags? @@ -426,11 +467,11 @@ see what it autoconfigures. What if I need to add a priority to my tag? Many autoconfigured tags have an optional priority. If you need to specify a priority -(or any other optional tag attribute), no problem! Just :ref:`manually configure your service ` +(or any other optional tag attribute), no problem! :ref:`Manually configure your service ` and add the tag. Your tag will take precedence over the one added by auto-configuration. -5) Auto-configure with ``_instanceof`` --------------------------------------- +5) Auto-configure with _instanceof +---------------------------------- And the final big change is ``_instanceof``. It acts as a default definition template (see `service-33-default_definition`_), but only for services whose @@ -443,18 +484,18 @@ inherited from an abstract definition: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # ... _instanceof: - AppBundle\Domain\LoaderInterface: + App\Domain\LoaderInterface: public: true tags: ['app.domain_loader'] .. code-block:: xml - + - + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Domain\LoaderInterface; + + return function(ContainerConfigurator $configurator) { + // ... + + $services->instanceof(LoaderInterface::class) + ->public() + ->tag('app.domain_loader'); + }; + What about Performance ---------------------- @@ -493,23 +549,23 @@ Ready to upgrade your existing project? Great! Suppose you have the following co .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: app.github_notifier: - class: AppBundle\Service\GitHubNotifier + class: App\Service\GitHubNotifier arguments: - '@app.api_client_github' markdown_transformer: - class: AppBundle\Service\MarkdownTransformer + class: App\Service\MarkdownTransformer app.api_client_github: - class: AppBundle\Service\ApiClient + class: App\Service\ApiClient arguments: - 'https://api.github.com' app.api_client_sl_connect: - class: AppBundle\Service\ApiClient + class: App\Service\ApiClient arguments: - 'https://connect.symfony.com/api' @@ -523,7 +579,7 @@ Start by adding a ``_defaults`` section with ``autowire`` and ``autoconfigure``. .. code-block:: diff - # app/config/services.yml + # config/services.yaml services: + _defaults: + autowire: true @@ -548,19 +604,19 @@ Start by updating the service ids to class names: .. code-block:: diff - # app/config/services.yml + # config/services.yaml services: # ... - app.github_notifier: - - class: AppBundle\Service\GitHubNotifier - + AppBundle\Service\GitHubNotifier: + - class: App\Service\GitHubNotifier + + App\Service\GitHubNotifier: arguments: - '@app.api_client_github' - markdown_transformer: - - class: AppBundle\Service\MarkdownTransformer - + AppBundle\Service\MarkdownTransformer: ~ + - class: App\Service\MarkdownTransformer + + App\Service\MarkdownTransformer: ~ # keep these ids because there are multiple instances per class app.api_client_github: @@ -596,30 +652,30 @@ Start by updating the service ids to class names: But, this change will break our app! The old service ids (e.g. ``app.github_notifier``) no longer exist. The simplest way to fix this is to find all your old service ids -and update them to the new class id: ``app.github_notifier`` to ``AppBundle\Service\GitHubNotifier``. +and update them to the new class id: ``app.github_notifier`` to ``App\Service\GitHubNotifier``. In large projects, there's a better way: create legacy aliases that map the old id -to the new id. Create a new ``legacy_aliases.yml`` file: +to the new id. Create a new ``legacy_aliases.yaml`` file: .. code-block:: yaml - # app/config/legacy_aliases.yml + # config/legacy_aliases.yaml services: _defaults: public: true # aliases so that the old service ids can still be accessed # remove these if/when you are not fetching these directly # from the container via $container->get() - app.github_notifier: '@AppBundle\Service\GitHubNotifier' - markdown_transformer: '@AppBundle\Service\MarkdownTransformer' + app.github_notifier: '@App\Service\GitHubNotifier' + markdown_transformer: '@App\Service\MarkdownTransformer' -Then import this at the top of ``services.yml``: +Then import this at the top of ``services.yaml``: .. code-block:: diff - # app/config/services.yml + # config/services.yaml + imports: - + - { resource: legacy_aliases.yml } + + - { resource: legacy_aliases.yaml } # ... @@ -633,7 +689,7 @@ Now you're ready to default all services to be private: .. code-block:: diff - # app/config/services.yml + # config/services.yaml # ... services: @@ -643,7 +699,7 @@ Now you're ready to default all services to be private: + public: false Thanks to this, any services created in this file cannot be fetched directly from -the container. But, since the old service id's are aliases in a separate file (``legacy_aliases.yml``), +the container. But, since the old service id's are aliases in a separate file (``legacy_aliases.yaml``), these *are* still public. This makes sure the app keeps working. If you did *not* change the id of some of your services (because there are multiple @@ -651,7 +707,7 @@ instances of the same class), you may need to make those public: .. code-block:: diff - # app/config/services.yml + # config/services.yaml # ... services: @@ -675,29 +731,29 @@ clean that up. Step 4) Auto-registering Services ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You're now ready to automatically register all services in ``src/AppBundle/`` +You're now ready to automatically register all services in ``src/`` (and/or any other directory/bundle you have): .. code-block:: diff - # app/config/services.yml + # config/services.yaml services: _defaults: # ... - + AppBundle\: - + resource: '../../src/AppBundle/*' - + exclude: '../../src/AppBundle/{Entity,Repository}' + + App\: + + resource: '../src/*' + + exclude: '../src/{Entity,Migrations,Tests}' + - + AppBundle\Controller\: - + resource: '../../src/AppBundle/Controller' + + App\Controller\: + + resource: '../src/Controller' + tags: ['controller.service_arguments'] # ... That's it! Actually, you're already overriding and reconfiguring all the services -you're using (``AppBundle\Service\GitHubNotifier`` and ``AppBundle\Service\MarkdownTransformer``). +you're using (``App\Service\GitHubNotifier`` and ``App\Service\MarkdownTransformer``). But now, you won't need to manually register future services. Once again, there is one extra complication if you have multiple services of the @@ -705,14 +761,14 @@ same class: .. code-block:: diff - # app/config/services.yml + # config/services.yaml services: # ... + # alias ApiClient to one of our services below + # app.api_client_github will be used to autowire ApiClient type-hints - + AppBundle\Service\ApiClient: '@app.api_client_github' + + App\Service\ApiClient: '@app.api_client_github' app.api_client_github: # ... @@ -731,14 +787,14 @@ Step 5) Cleanup! To make sure your application didn't break, you did some extra work. Now it's time to clean things up! First, update your application to *not* use the old service id's (the -ones in ``legacy_aliases.yml``). This means updating any service arguments (e.g. -``@app.github_notifier`` to ``@AppBundle\Service\GitHubNotifier``) and updating your +ones in ``legacy_aliases.yaml``). This means updating any service arguments (e.g. +``@app.github_notifier`` to ``@App\Service\GitHubNotifier``) and updating your code to not fetch this service directly from the container. For example: .. code-block:: diff - - public function indexAction() - + public function indexAction(GitHubNotifier $gitHubNotifier, MarkdownTransformer $markdownTransformer) + - public function index() + + public function index(GitHubNotifier $gitHubNotifier, MarkdownTransformer $markdownTransformer) { - // the old way of fetching services - $githubNotifier = $this->container->get('app.github_notifier'); @@ -747,14 +803,14 @@ code to not fetch this service directly from the container. For example: // ... } -As soon as you do this, you can delete ``legacy_aliases.yml`` and remove its import. +As soon as you do this, you can delete ``legacy_aliases.yaml`` and remove its import. You should do the same thing for any services that you made public, like ``app.api_client_github`` and ``app.api_client_sl_connect``. Once you're not fetching these directly from the container, you can remove the ``public: true`` flag: .. code-block:: diff - # app/config/services.yml + # config/services.yaml services: # ... @@ -766,7 +822,7 @@ these directly from the container, you can remove the ``public: true`` flag: # ... - public: true -Finally, you can optionally remove any services from ``services.yml`` whose arguments +Finally, you can optionally remove any services from ``services.yaml`` whose arguments can be autowired. The final configuration looks like this: .. code-block:: yaml @@ -777,31 +833,31 @@ can be autowired. The final configuration looks like this: autoconfigure: true public: false - AppBundle\: - resource: '../../src/AppBundle/*' - exclude: '../../src/AppBundle/{Entity,Repository}' + App\: + resource: '../src/*' + exclude: '../src/{Entity,Migrations,Tests}' - AppBundle\Controller\: - resource: '../../src/AppBundle/Controller' + App\Controller\: + resource: '../src/Controller' tags: ['controller.service_arguments'] - AppBundle\Service\GitHubNotifier: + App\Service\GitHubNotifier: # this could be deleted, or I can keep being explicit arguments: - '@app.api_client_github' # alias ApiClient to one of our services below # app.api_client_github will be used to autowire ApiClient type-hints - AppBundle\Service\ApiClient: '@app.api_client_github' + App\Service\ApiClient: '@app.api_client_github' # keep these ids because there are multiple instances per class app.api_client_github: - class: AppBundle\Service\ApiClient + class: App\Service\ApiClient arguments: - 'https://api.github.com' app.api_client_sl_connect: - class: AppBundle\Service\ApiClient + class: App\Service\ApiClient arguments: - 'https://connect.symfony.com/api' diff --git a/service_container/alias_private.rst b/service_container/alias_private.rst index 381fec0502b..ce7a1251ebe 100644 --- a/service_container/alias_private.rst +++ b/service_container/alias_private.rst @@ -23,7 +23,7 @@ And in this case, those services do *not* need to be public. So unless you *specifically* need to access a service directly from the container via ``$container->get()``, the best-practice is to make your services *private*. -In fact, the :ref:`default services.yml configuration ` configures +In fact, the :ref:`default services.yaml configuration ` configures all services to be private by default. You can also control the ``public`` option on a service-by-service basis: @@ -32,30 +32,39 @@ You can also control the ``public`` option on a service-by-service basis: .. code-block:: yaml + # config/services.yaml services: # ... - AppBundle\Service\Foo: + App\Service\Foo: public: false .. code-block:: xml + - + .. code-block:: php - use AppBundle\Service\Foo; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - $container->register(Foo::class) - ->setPublic(false); + use App\Service\Foo; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(Foo::class) + ->private(); + }; .. _services-why-private: @@ -68,7 +77,7 @@ not have run on that page. Now that the service is private, you *must not* fetch the service directly from the container:: - use AppBundle\Service\Foo; + use App\Service\Foo; $container->get(Foo::class); @@ -78,11 +87,6 @@ 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). -.. note:: - - Services are by default public, but it's considered a good practice to mark - as many services private as possible. - .. _services-alias: Aliasing @@ -96,17 +100,19 @@ services. .. code-block:: yaml + # config/services.yaml services: # ... - AppBundle\Mail\PhpMailer: + App\Mail\PhpMailer: public: false app.mailer: - alias: AppBundle\Mail\PhpMailer + alias: App\Mail\PhpMailer public: true .. code-block:: xml + - + - + .. code-block:: php - use AppBundle\Mail\PhpMailer; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - $container->register(PhpMailer::class) - ->setPublic(false); + use App\Mail\PhpMailer; - $container->setAlias('app.mailer', PhpMailer::class); + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(PhpMailer::class) + ->private(); + + $services->alias('app.mailer', PhpMailer::class); + }; This means that when using the container directly, you can access the ``PhpMailer`` service by asking for the ``app.mailer`` service like this:: @@ -140,20 +153,72 @@ This means that when using the container directly, you can access the .. code-block:: yaml + # config/services.yaml services: # ... - app.mailer: '@AppBundle\Mail\PhpMailer' + app.mailer: '@App\Mail\PhpMailer' -Anonymous Services ------------------- +Deprecating Service Aliases +~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. note:: +If you decide to deprecate the use of a service alias (because it is outdated +or you decided not to maintain it anymore), you can deprecate its definition: + +.. configuration-block:: + + .. code-block:: yaml + + app.mailer: + alias: '@App\Mail\PhpMailer' + + # this will display a generic deprecation message... + deprecated: true + + # ...but you can also define a custom deprecation message + deprecated: 'The "%alias_id%" alias is deprecated. Do not use it anymore.' + + .. code-block:: xml + + + + + + + + + + + The "%alias_id%" service alias is deprecated. Don't use it anymore. + + + + + .. code-block:: php + + $container + ->setAlias('app.mailer', 'App\Mail\PhpMailer') - Anonymous services are only supported by the XML and YAML configuration formats. + // this will display a generic deprecation message... + ->setDeprecated(true) -.. versionadded:: 3.3 + // ...but you can also define a custom deprecation message + ->setDeprecated( + true, + 'The "%alias_id%" service alias is deprecated. Don\'t use it anymore.' + ) + ; + +Now, every time this service alias is used, a deprecation warning is triggered, +advising you to stop or to change your uses of that alias. - The feature to configure anonymous services in YAML was introduced in Symfony 3.3. +The message is actually a message template, which replaces occurrences of the +``%alias_id%`` placeholder by the service alias id. You **must** have at least +one occurrence of the ``%alias_id%`` placeholder in your template. + +Anonymous Services +------------------ In some cases, you may want to prevent a service being used as a dependency of other services. This can be achieved by creating an anonymous service. These @@ -166,19 +231,16 @@ The following example shows how to inject an anonymous service into another serv .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: - _defaults: - autowire: true - - AppBundle\Foo: + App\Foo: arguments: - !service - class: AppBundle\AnonymousBar + class: App\AnonymousBar .. code-block:: xml - + - - - + - + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\AnonymousBar; + use App\Foo; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(Foo::class) + ->args([inline(AnonymousBar::class)]) + }; + +.. note:: + + Anonymous services do *NOT* inherit the definitions provided from the + defaults defined in the configuration. So you'll need to explicitly mark + service as autowired or autoconfigured when doing an anonymous service + e.g.: ``inline(Foo::class)->autowire()->autoconfigure()``. + Using an anonymous service as a factory looks like this: .. configuration-block:: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: - _defaults: - autowire: true - - AppBundle\Foo: - factory: [ !service { class: AppBundle\FooFactory }, 'constructFoo' ] + App\Foo: + factory: [ !service { class: App\FooFactory }, 'constructFoo' ] .. code-block:: xml - + - - - + @@ -230,6 +307,21 @@ Using an anonymous service as a factory looks like this: + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\AnonymousBar; + use App\Foo; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(Foo::class) + ->factory([inline(AnonymousBar::class), 'constructFoo']) + }; + Deprecating Services -------------------- @@ -240,18 +332,20 @@ or you decided not to maintain it anymore), you can deprecate its definition: .. code-block:: yaml - AppBundle\Service\OldService: + # config/services.yaml + App\Service\OldService: deprecated: The "%service_id%" service is deprecated since vendor-name/package-name 2.8 and will be removed in 3.0. .. code-block:: xml + - + The "%service_id%" service is deprecated since vendor-name/package-name 2.8 and will be removed in 3.0. @@ -259,15 +353,17 @@ or you decided not to maintain it anymore), you can deprecate its definition: .. code-block:: php - use AppBundle\Service\OldService; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - $container - ->register(OldService::class) - ->setDeprecated( - true, - 'The "%service_id%" service is deprecated since vendor-name/package-name 2.8 and will be removed in 3.0.' - ) - ; + use App\Service\OldService; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(OldService::class) + ->deprecate('The "%service_id%" service is deprecated since vendor-name/package-name 2.8 and will be removed in 3.0.'); + }; Now, every time this service is used, a deprecation warning is triggered, advising you to stop or to change your uses of that service. diff --git a/service_container/autowiring.rst b/service_container/autowiring.rst index effbbea9856..4d4a32cfc63 100644 --- a/service_container/autowiring.rst +++ b/service_container/autowiring.rst @@ -24,7 +24,7 @@ the alphabet. Start by creating a ROT13 transformer class:: - namespace AppBundle\Util; + namespace App\Util; class Rot13Transformer { @@ -36,9 +36,9 @@ Start by creating a ROT13 transformer class:: And now a Twitter client using this transformer:: - namespace AppBundle\Service; + namespace App\Service; - use AppBundle\Util\Rot13Transformer; + use App\Util\Rot13Transformer; class TwitterClient { @@ -57,7 +57,7 @@ And now a Twitter client using this transformer:: } } -If you're using the :ref:`default services.yml configuration `, +If you're using the :ref:`default services.yaml configuration `, **both classes are automatically registered as services and configured to be autowired**. This means you can use them immediately without *any* configuration. @@ -68,6 +68,7 @@ both services: .. code-block:: yaml + # config/services.yaml services: _defaults: autowire: true @@ -75,15 +76,16 @@ both services: public: false # ... - AppBundle\Service\TwitterClient: + App\Service\TwitterClient: # redundant thanks to _defaults, but value is overridable on each service autowire: true - AppBundle\Util\Rot13Transformer: + App\Util\Rot13Transformer: autowire: true .. code-block:: xml + - + + - + .. code-block:: php - use AppBundle\Service\TwitterClient; - use AppBundle\Util\Rot13Transformer; + // config/services.php + return function(ContainerConfigurator $configurator) { + $services = $configurator->services() + ->defaults() + ->autowire() + ->autoconfigure() + ; - // ... + $services->set(TwitterClient::class) + // redundant thanks to defaults, but value is overridable on each service + ->autowire(); - // the autowire method is new in Symfony 3.3 - // in earlier versions, use register() and then call setAutowired(true) - $container->autowire(TwitterClient::class); + $services->set(Rot13Transformer::class) + ->autowire(); + }; - $container->autowire(Rot13Transformer::class) - ->setPublic(false); Now, you can use the ``TwitterClient`` service immediately in a controller:: - namespace AppBundle\Controller; + namespace App\Controller; - use AppBundle\Service\TwitterClient; - use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use App\Service\TwitterClient; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; - class DefaultController extends Controller + class DefaultController extends AbstractController { /** * @Route("/tweet", methods={"POST"}) */ - public function tweetAction(TwitterClient $twitterClient) + public function tweet(TwitterClient $twitterClient) { // fetch $user, $key, $status from the POST'ed data @@ -147,7 +155,7 @@ Autowiring Logic Explained Autowiring works by reading the ``Rot13Transformer`` *type-hint* in ``TwitterClient``:: // ... - use AppBundle\Util\Rot13Transformer; + use App\Util\Rot13Transformer; class TwitterClient { @@ -160,29 +168,14 @@ Autowiring works by reading the ``Rot13Transformer`` *type-hint* in ``TwitterCli } The autowiring system **looks for a service whose id exactly matches the type-hint**: -so ``AppBundle\Util\Rot13Transformer``. In this case, that exists! When you configured +so ``App\Util\Rot13Transformer``. In this case, that exists! When you configured the ``Rot13Transformer`` service, you used its fully-qualified class name as its id. Autowiring isn't magic: it looks for a service whose id matches the type-hint. If you :ref:`load services automatically `, -each service's id is its class name. This is the main way to control autowiring. - -If there is *not* a service whose id exactly matches the type, then: - -If there are **0** services in the container that have the type, then: - If the type is a concrete class, then a new, private, autowired service is - auto-registered in the container and used for the argument. - -.. _autowiring-single-matching-service: - -If there is exactly **1** service in the container that has the type, then: - (deprecated) This service is used for the argument. In Symfony 4.0, this - will be removed. The proper solution is to create an :ref:`alias ` - from the type to the service id so that normal autowiring works. +each service's id is its class name. -If there are **2 or more** services in the container that have the type, then: - A clear exception is thrown. You need to *choose* which service should - be used by creating an :ref:`alias ` or - :ref:`configuring the argument explicitly `. +If there is *not* a service whose id exactly matches the type, a clear exception +will be thrown. Autowiring is a great way to automate configuration, and Symfony tries to be as *predictable* and clear as possible. @@ -193,13 +186,13 @@ Using Aliases to Enable Autowiring ---------------------------------- The main way to configure autowiring is to create a service whose id exactly matches -its class. In the previous example, the service's id is ``AppBundle\Util\Rot13Transformer``, +its class. In the previous example, the service's id is ``App\Util\Rot13Transformer``, which allows us to autowire this type automatically. This can also be accomplished using an :ref:`alias `. Suppose that for some reason, the id of the service was instead ``app.rot13.transformer``. In -this case, any arguments type-hinted with the class name (``AppBundle\Util\Rot13Transformer``) -can no longer be autowired (actually, it :ref:`will work now, but not in Symfony 4.0 `). +this case, any arguments type-hinted with the class name (``App\Util\Rot13Transformer``) +can no longer be autowired. No problem! To fix this, you can *create* a service whose id matches the class by adding a service alias: @@ -208,21 +201,23 @@ adding a service alias: .. code-block:: yaml + # config/services.yaml services: # ... # the id is not a class, so it won't be used for autowiring app.rot13.transformer: - class: AppBundle\Util\Rot13Transformer + class: App\Util\Rot13Transformer # ... # but this fixes it! # the ``app.rot13.transformer`` service will be injected when - # an ``AppBundle\Util\Rot13Transformer`` type-hint is detected - AppBundle\Util\Rot13Transformer: '@app.rot13.transformer' + # an ``App\Util\Rot13Transformer`` type-hint is detected + App\Util\Rot13Transformer: '@app.rot13.transformer' .. code-block:: xml + - - + + .. code-block:: php - use AppBundle\Util\Rot13Transformer; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - // ... + use App\Util\Rot13Transformer; + + return function(ContainerConfigurator $configurator) { + // ... + + // the id is not a class, so it won't be used for autowiring + $services->set('app.rot13.transformer', Rot13Transformer::class) + ->autowire(); - $container->autowire('app.rot13.transformer', Rot13Transformer::class) - ->setPublic(false); - $container->setAlias(Rot13Transformer::class, 'app.rot13.transformer'); + // but this fixes it! + // the ``app.rot13.transformer`` service will be injected when + // an ``App\Util\Rot13Transformer`` type-hint is detected + $services->alias(Rot13Transformer::class, 'app.rot13.transformer'); + }; -This creates a service "alias", whose id is ``AppBundle\Util\Rot13Transformer``. + +This creates a service "alias", whose id is ``App\Util\Rot13Transformer``. Thanks to this, autowiring sees this and uses it whenever the ``Rot13Transformer`` class is type-hinted. @@ -267,7 +273,7 @@ of concrete classes as it replaces your dependencies with other objects. To follow this best practice, suppose you decide to create a ``TransformerInterface``:: - namespace AppBundle\Util; + namespace App\Util; interface TransformerInterface { @@ -294,10 +300,9 @@ Now that you have an interface, you should use this as your type-hint:: // ... } -But now, the type-hint (``AppBundle\Util\TransformerInterface``) no longer matches -the id of the service (``AppBundle\Util\Rot13Transformer``). This means that the -argument can no longer be autowired (actually, it -:ref:`will work now, but not in Symfony 4.0 `). +But now, the type-hint (``App\Util\TransformerInterface``) no longer matches +the id of the service (``App\Util\Rot13Transformer``). This means that the +argument can no longer be autowired. To fix that, add an :ref:`alias `: @@ -305,17 +310,19 @@ To fix that, add an :ref:`alias `: .. code-block:: yaml + # config/services.yaml services: # ... - AppBundle\Util\Rot13Transformer: ~ + App\Util\Rot13Transformer: ~ - # the ``AppBundle\Util\Rot13Transformer`` service will be injected when - # an ``AppBundle\Util\TransformerInterface`` type-hint is detected - AppBundle\Util\TransformerInterface: '@AppBundle\Util\Rot13Transformer' + # the ``App\Util\Rot13Transformer`` service will be injected when + # an ``App\Util\TransformerInterface`` type-hint is detected + App\Util\TransformerInterface: '@App\Util\Rot13Transformer' .. code-block:: xml + `: - + - + .. code-block:: php - use AppBundle\Util\Rot13Transformer; - use AppBundle\Util\TransformerInterface; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - // ... - $container->autowire(Rot13Transformer::class); - $container->setAlias(TransformerInterface::class, Rot13Transformer::class); + use App\Util\Rot13Transformer; + use App\Util\TransformerInterface; + + return function(ContainerConfigurator $configurator) { + // ... + + $services->set(Rot13Transformer::class); -Thanks to the ``AppBundle\Util\TransformerInterface`` alias, the autowiring subsystem -knows that the ``AppBundle\Util\Rot13Transformer`` service should be injected when + // the ``App\Util\Rot13Transformer`` service will be injected when + // an ``App\Util\TransformerInterface`` type-hint is detected + $services->alias(TransformerInterface::class, Rot13Transformer::class); + }; + + +Thanks to the ``App\Util\TransformerInterface`` alias, the autowiring subsystem +knows that the ``App\Util\Rot13Transformer`` service should be injected when dealing with the ``TransformerInterface``. .. tip:: When using a `service definition prototype`_, if only one service is discovered that implements an interface, and that interface is also - discovered at the same time, configuring the alias is not mandatory + discovered in the same file, configuring the alias is not mandatory and Symfony will automatically create one. Dealing with Multiple Implementations of the Same Type @@ -355,7 +372,7 @@ Dealing with Multiple Implementations of the Same Type Suppose you create a second class - ``UppercaseTransformer`` that implements ``TransformerInterface``:: - namespace AppBundle\Util; + namespace App\Util; class UppercaseTransformer implements TransformerInterface { @@ -366,39 +383,77 @@ Suppose you create a second class - ``UppercaseTransformer`` that implements } If you register this as a service, you now have *two* services that implement the -``AppBundle\Util\TransformerInterface`` type. Autowiring subsystem can not decide +``App\Util\TransformerInterface`` type. Autowiring subsystem can not decide which one to use. Remember, autowiring isn't magic; it looks for a service whose id matches the type-hint. So you need to choose one by creating an alias from the type to the correct service id (see :ref:`autowiring-interface-alias`). +Additionally, you can define several named autowiring aliases if you want to use +one implementation in some cases, and another implementation in some +other cases. + +For instance, you may want to use by default the ``Rot13Transformer`` +implementation by default when the ``TransformerInterface`` interface is +type hinted, but use the ``UppercaseTransformer`` implementation in some +specific cases. To do so, you can create a normal alias from the +``TransformerInterface`` interface to ``Rot13Transformer``, and then +create a *named autowiring alias* from a special string containing the +interface followed by a variable name matching the one you use when doing +the injection:: + + namespace App\Service; -If you want ``Rot13Transformer`` to be the service that's used for autowiring, create -that alias: + use App\Util\TransformerInterface; + + class MastodonClient + { + private $transformer; + + public function __construct(TransformerInterface $shoutyTransformer) + { + $this->transformer = $shoutyTransformer; + } + + public function toot($user, $key, $status) + { + $transformedStatus = $this->transformer->transform($status); + + // ... connect to Mastodon and send the transformed status + } + } .. configuration-block:: .. code-block:: yaml + # config/services.yaml services: # ... - AppBundle\Util\Rot13Transformer: ~ - AppBundle\Util\UppercaseTransformer: ~ + App\Util\Rot13Transformer: ~ + App\Util\UppercaseTransformer: ~ + + # the ``App\Util\UppercaseTransformer`` service will be + # injected when an ``App\Util\TransformerInterface`` + # type-hint for a ``$shoutyTransformer`` argument is detected. + App\Util\TransformerInterface $shoutyTransformer: '@App\Util\UppercaseTransformer' - # the ``AppBundle\Util\Rot13Transformer`` service will be injected when - # a ``AppBundle\Util\TransformerInterface`` type-hint is detected - AppBundle\Util\TransformerInterface: '@AppBundle\Util\Rot13Transformer' + # If the argument used for injection does not match, but the + # type-hint still matches, the ``App\Util\Rot13Transformer`` + # service will be injected. + App\Util\TransformerInterface: '@App\Util\Rot13Transformer' - AppBundle\Service\TwitterClient: + App\Service\TwitterClient: # the Rot13Transformer will be passed as the $transformer argument autowire: true - # If you wanted to choose the non-default service, wire it manually - # arguments: - # $transformer: '@AppBundle\Util\UppercaseTransformer' + # If you wanted to choose the non-default service and do not + # want to use a named autowiring alias, wire it manually: + # $transformer: '@App\Util\UppercaseTransformer' # ... .. code-block:: xml + - - + + - + + - - + + .. code-block:: php - use AppBundle\Service\TwitterClient; - use AppBundle\Util\Rot13Transformer; - use AppBundle\Util\TransformerInterface; - use AppBundle\Util\UppercaseTransformer; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - // ... - $container->autowire(Rot13Transformer::class); - $container->autowire(UppercaseTransformer::class); - $container->setAlias(TransformerInterface::class, Rot13Transformer::class); - $container->autowire(TwitterClient::class) - //->setArgument('$transformer', new Reference(UppercaseTransformer::class)) - ; - -Thanks to the ``AppBundle\Util\TransformerInterface`` alias, any argument type-hinted -with this interface will be passed the ``AppBundle\Util\Rot13Transformer`` service. -But, you can also manually wire the *other* service by specifying the argument -under the arguments key. + use App\Service\MastodonClient; + use App\Service\TwitterClient; + use App\Util\Rot13Transformer; + use App\Util\TransformerInterface; + use App\Util\UppercaseTransformer; -.. versionadded:: 3.3 + return function(ContainerConfigurator $configurator) { + // ... - Using FQCN aliases to fix autowiring ambiguities was introduced in Symfony - 3.3. Prior to version 3.3, you needed to use the ``autowiring_types`` key. + $services->set(Rot13Transformer::class)->autowire(); + $services->set(UppercaseTransformer::class)->autowire(); + + // the ``App\Util\UppercaseTransformer`` service will be + // injected when an ``App\Util\TransformerInterface`` + // type-hint for a ``$shoutyTransformer`` argument is detected. + $services->alias(TransformerInterface::class.' $shoutyTransformer', UppercaseTransformer::class); + + // If the argument used for injection does not match, but the + // type-hint still matches, the ``App\Util\Rot13Transformer`` + // service will be injected. + $services->alias(TransformerInterface::class, Rot13Transformer::class); + + $services->set(TwitterClient::class) + // the Rot13Transformer will be passed as the $transformer argument + ->autowire() + + // If you wanted to choose the non-default service and do not + // want to use a named autowiring alias, wire it manually: + // ->arg('$transformer', ref(UppercaseTransformer::class)) + // ... + }; + +Thanks to the ``App\Util\TransformerInterface`` alias, any argument type-hinted +with this interface will be passed the ``App\Util\Rot13Transformer`` service. +If the argument is named ``$shoutyTransformer``, +``App\Util\UppercaseTransformer`` will be used instead. +But, you can also manually wire any *other* service by specifying the argument +under the arguments key. Fixing Non-Autowireable Arguments --------------------------------- @@ -461,7 +538,7 @@ When autowiring is enabled for a service, you can *also* configure the container to call methods on your class when it's instantiated. For example, suppose you want to inject the ``logger`` service, and decide to use setter-injection:: - namespace AppBundle\Util; + namespace App\Util; class Rot13Transformer { diff --git a/service_container/calls.rst b/service_container/calls.rst index c2f6d6c2b92..448be1f6623 100644 --- a/service_container/calls.rst +++ b/service_container/calls.rst @@ -13,7 +13,7 @@ Usually, you'll want to inject your dependencies via the constructor. But someti especially if a dependency is optional, you may want to use "setter injection". For example:: - namespace AppBundle\Service; + namespace App\Service; use Psr\Log\LoggerInterface; @@ -35,9 +35,9 @@ To configure the container to call the ``setLogger`` method, use the ``calls`` k .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: - AppBundle\Service\MessageGenerator: + App\Service\MessageGenerator: # ... calls: - method: setLogger @@ -46,7 +46,7 @@ To configure the container to call the ``setLogger`` method, use the ``calls`` k .. code-block:: xml - + - + @@ -65,9 +65,87 @@ To configure the container to call the ``setLogger`` method, use the ``calls`` k .. code-block:: php - // app/config/services.php - use AppBundle\Service\MessageGenerator; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Service\MessageGenerator; + + return function(ContainerConfigurator $configurator) { + // ... + + $services->set(MessageGenerator::class) + ->call('setLogger', [ref('logger')]); + }; + + +To provide immutable services, some classes implement immutable setters. +Such setters return a new instance of the configured class +instead of mutating the object they were called on:: + + namespace App\Service; + + use Psr\Log\LoggerInterface; + + class MessageGenerator + { + private $logger; + + /** + * @return static + */ + public function withLogger(LoggerInterface $logger) + { + $new = clone $this; + $new->logger = $logger; + + return $new; + } + + // ... + } + +Because the method returns a separate cloned instance, configuring such a service means using +the return value of the wither method (``$service = $service->withLogger($logger);``). +The configuration to tell the container it should do so would be like: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + App\Service\MessageGenerator: + # ... + calls: + - method: withLogger + arguments: + - '@logger' + returns_clone: true + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + use App\Service\MessageGenerator; use Symfony\Component\DependencyInjection\Reference; $container->register(MessageGenerator::class) - ->addMethodCall('setLogger', [new Reference('logger')]); + ->addMethodCall('withLogger', [new Reference('logger')], true); diff --git a/service_container/compiler_passes.rst b/service_container/compiler_passes.rst index c76c4f3d5a8..4d959e93dc6 100644 --- a/service_container/compiler_passes.rst +++ b/service_container/compiler_passes.rst @@ -2,26 +2,84 @@ single: DependencyInjection; Compiler passes single: Service Container; Compiler passes -How to Work with Compiler Passes in Bundles -=========================================== +How to Work with Compiler Passes +================================ Compiler passes give you an opportunity to manipulate other :doc:`service definitions ` that have been registered with the service container. You can read about how to create them in the components section ":ref:`components-di-separate-compiler-passes`". -When using :ref:`separate compiler passes `, -you need to register them in the ``build()`` method of the bundle class (this -is not needed when implementing the ``process()`` method in the extension):: +Compiler passes are registered in the ``build()`` method of the application kernel:: - // src/AppBundle/AppBundle.php - namespace AppBundle; + // src/Kernel.php + namespace App; - use AppBundle\DependencyInjection\Compiler\CustomPass; + use App\DependencyInjection\Compiler\CustomPass; + use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\HttpKernel\Kernel as BaseKernel; + + class Kernel extends BaseKernel + { + use MicroKernelTrait; + + // ... + + protected function build(ContainerBuilder $container): void + { + $container->addCompilerPass(new CustomPass()); + } + } + +One of the most common use-cases of compiler passes is to work with :doc:`tagged +services `. In those cases, instead of creating a +compiler pass, you can make the kernel implement +:class:`Symfony\\Component\\DependencyInjection\\Compiler\\CompilerPassInterface` +and process the services inside the ``process()`` method:: + + // src/Kernel.php + namespace App; + + use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; + use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\HttpKernel\Kernel as BaseKernel; + + class Kernel extends BaseKernel implements CompilerPassInterface + { + use MicroKernelTrait; + + // ... + + public function process(ContainerBuilder $container) + { + // in this method you can manipulate the service container: + // for example, changing some container service: + $container->getDefinition('app.some_private_service')->setPublic(true); + + // or processing tagged services: + foreach ($container->findTaggedServiceIds('some_tag') as $id => $tags) { + // ... + } + } + } + +Working with Compiler Passes in Bundles +--------------------------------------- + +:doc:`Bundles ` can define compiler passes in the ``build()`` method of +the main bundle class (this is not needed when implementing the ``process()`` +method in the extension):: + + // src/MyBundle/MyBundle.php + namespace App\MyBundle; + + use App\DependencyInjection\Compiler\CustomPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; - class AppBundle extends Bundle + class MyBundle extends Bundle { public function build(ContainerBuilder $container) { @@ -31,10 +89,8 @@ is not needed when implementing the ``process()`` method in the extension):: } } -One of the most common use-cases of compiler passes is to work with -":doc:`service tags `". 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``. +If you are using custom :doc:`service 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``. diff --git a/service_container/configurators.rst b/service_container/configurators.rst index 43b4d55e8a5..ec5e566fbbb 100644 --- a/service_container/configurators.rst +++ b/service_container/configurators.rst @@ -21,8 +21,8 @@ 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:: - // src/AppBundle/Mail/NewsletterManager.php - namespace AppBundle\Mail; + // src/Mail/NewsletterManager.php + namespace App\Mail; class NewsletterManager implements EmailFormatterAwareInterface { @@ -38,8 +38,8 @@ You start defining a ``NewsletterManager`` class like this:: and also a ``GreetingCardManager`` class:: - // src/AppBundle/Mail/GreetingCardManager.php - namespace AppBundle\Mail; + // src/Mail/GreetingCardManager.php + namespace App\Mail; class GreetingCardManager implements EmailFormatterAwareInterface { @@ -58,8 +58,8 @@ on application settings. To do this, you also have an ``EmailFormatterManager`` class which is responsible for loading and validating formatters enabled in the application:: - // src/AppBundle/Mail/EmailFormatterManager.php - namespace AppBundle\Mail; + // src/Mail/EmailFormatterManager.php + namespace App\Mail; class EmailFormatterManager { @@ -80,8 +80,8 @@ 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:: - // src/AppBundle/Mail/EmailConfigurator.php - namespace AppBundle\Mail; + // src/Mail/EmailConfigurator.php + namespace App\Mail; class EmailConfigurator { @@ -117,7 +117,7 @@ Using the Configurator ---------------------- You can configure the service configurator using the ``configurator`` option. If -you're using the :ref:`default services.yml configuration `, +you're using the :ref:`default services.yaml configuration `, all the classes are already loaded as services. All you need to do is specify the ``configurator``: @@ -125,25 +125,25 @@ all the classes are already loaded as services. All you need to do is specify th .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # ... - # Registers all 4 classes as services, including AppBundle\Mail\EmailConfigurator - AppBundle\: - resource: '../../src/AppBundle/*' + # Registers all 4 classes as services, including App\Mail\EmailConfigurator + App\: + resource: '../src/*' # ... # override the services to set the configurator - AppBundle\Mail\NewsletterManager: - configurator: ['@AppBundle\Mail\EmailConfigurator', 'configure'] + App\Mail\NewsletterManager: + configurator: ['@App\Mail\EmailConfigurator', 'configure'] - AppBundle\Mail\GreetingCardManager: - configurator: ['@AppBundle\Mail\EmailConfigurator', 'configure'] + App\Mail\GreetingCardManager: + configurator: ['@App\Mail\EmailConfigurator', 'configure'] .. code-block:: xml - + - + - - + + - - + + .. code-block:: php - // app/config/services.php - use AppBundle\Mail\GreetingCardManager; - use AppBundle\Mail\NewsletterManager; - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - // Same as before - $definition = new Definition(); + use App\Mail\EmailConfigurator; + use App\Mail\GreetingCardManager; + use App\Mail\NewsletterManager; - $definition->setAutowired(true); + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); - $this->registerClasses($definition, 'AppBundle\\', '../../src/AppBundle/*'); + // Registers all 4 classes as services, including App\Mail\EmailConfigurator + $services->load('App\\', '../src/*'); - $container->getDefinition(NewsletterManager::class) - ->setConfigurator([new Reference(EmailConfigurator::class), 'configure']); + // override the services to set the configurator + $services->set(NewsletterManager::class) + ->configurator(ref(EmailConfigurator::class), 'configure'); - $container->getDefinition(GreetingCardManager::class) - ->setConfigurator([new Reference(EmailConfigurator::class), 'configure']); + $services->set(GreetingCardManager::class) + ->configurator(ref(EmailConfigurator::class), 'configure'); + }; -That's it! When requesting the ``AppBundle\Mail\NewsletterManager`` or -``AppBundle\Mail\GreetingCardManager`` service, the created instance will first be +.. _configurators-invokable: + +Services can be configured via invokable configurators (replacing the +``configure()`` method with ``__invoke()``) by omitting the method name, just as +routes can reference :ref:`invokable controllers `. + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + # registers all classes as services, including App\Mail\EmailConfigurator + App\: + resource: '../src/*' + # ... + + # override the services to set the configurator + App\Mail\NewsletterManager: + configurator: '@App\Mail\EmailConfigurator' + + App\Mail\GreetingCardManager: + configurator: '@App\Mail\EmailConfigurator' + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Mail\GreetingCardManager; + use App\Mail\NewsletterManager; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + // Registers all 4 classes as services, including App\Mail\EmailConfigurator + $services->load('App\\', '../src/*'); + + // override the services to set the configurator + $services->set(NewsletterManager::class) + ->configurator(ref(EmailConfigurator::class)); + + $services->set(GreetingCardManager::class) + ->configurator(ref(EmailConfigurator::class)); + }; + +That's it! When requesting the ``App\Mail\NewsletterManager`` or +``App\Mail\GreetingCardManager`` service, the created instance will first be passed to the ``EmailConfigurator::configure()`` method. diff --git a/service_container/debug.rst b/service_container/debug.rst index b8c3ec5fd61..635bbdfa9ae 100644 --- a/service_container/debug.rst +++ b/service_container/debug.rst @@ -6,17 +6,14 @@ How to Debug the Service Container & List 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: +console. To show all services (public and private) and their PHP classes, run: .. code-block:: terminal $ php bin/console debug:container -By default, only public services are shown, but you can also view private services: - -.. code-block:: terminal - - $ php bin/console debug:container --show-private + # add this option to display "hidden services" too (those whose ID starts with a dot) + $ php bin/console debug:container --show-hidden To see a list of all of the available types that can be used for autowiring, run: @@ -24,10 +21,6 @@ To see a list of all of the available types that can be used for autowiring, run $ php bin/console debug:autowiring -.. versionadded:: 3.4 - - The ``debug:autowiring`` command was introduced in Symfony 3.3. - Detailed Info about a Single Service ------------------------------------ @@ -36,11 +29,7 @@ its id: .. code-block:: terminal - $ php bin/console debug:container 'AppBundle\Service\Mailer' + $ php bin/console debug:container 'App\Service\Mailer' # to show the service arguments: - $ php bin/console debug:container 'AppBundle\Service\Mailer' --show-arguments - -.. versionadded:: 3.3 - - The ``--show-arguments`` option was introduced in Symfony 3.3. + $ php bin/console debug:container 'App\Service\Mailer' --show-arguments diff --git a/service_container/definitions.rst b/service_container/definitions.rst index 5e74bef8ec9..48ebd7b8818 100644 --- a/service_container/definitions.rst +++ b/service_container/definitions.rst @@ -34,11 +34,11 @@ There are some helpful methods for working with the service definitions:: $definition = $container->findDefinition('app.user_config_manager'); // adds a new "app.number_generator" definition - $definition = new Definition(\AppBundle\NumberGenerator::class); + $definition = new Definition(\App\NumberGenerator::class); $container->setDefinition('app.number_generator', $definition); // shortcut for the previous method - $container->register('app.number_generator', \AppBundle\NumberGenerator::class); + $container->register('app.number_generator', \App\NumberGenerator::class); Working with a Definition ------------------------- @@ -56,8 +56,8 @@ Class The first optional argument of the ``Definition`` class is the fully qualified class name of the object returned when the service is fetched from the container:: - use AppBundle\Config\CustomConfigManager; - use AppBundle\Config\UserConfigManager; + use App\Config\CustomConfigManager; + use App\Config\UserConfigManager; use Symfony\Component\DependencyInjection\Definition; $definition = new Definition(UserConfigManager::class); @@ -75,7 +75,7 @@ The second optional argument of the ``Definition`` class is an array with the arguments passed to the constructor of the object returned when the service is fetched from the container:: - use AppBundle\Config\DoctrineConfigManager; + use App\Config\DoctrineConfigManager; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; @@ -117,6 +117,9 @@ any method calls in the definitions as well:: // configures a new method call $definition->addMethodCall('setLogger', [new Reference('logger')]); + // configures an immutable-setter + $definition->addMethodCall('withLogger', [new Reference('logger')], true); + // replaces all previously configured method calls with the passed array $definition->setMethodCalls($methodCalls); diff --git a/service_container/expression_language.rst b/service_container/expression_language.rst index fcf2e5e8c57..9a77cb1a817 100644 --- a/service_container/expression_language.rst +++ b/service_container/expression_language.rst @@ -10,30 +10,32 @@ How to Inject Values Based on Complex Expressions The service container also supports an "expression" that allows you to inject very specific values into a service. -For example, suppose you have a service (not shown here), called ``AppBundle\Mail\MailerConfiguration``, +For example, suppose you have a service (not shown here), called ``App\Mail\MailerConfiguration``, which has a ``getMailerMethod()`` method on it. This returns a string - like ``sendmail`` based on some configuration. Suppose that you want to pass the result of this method as a constructor argument -to another service: ``AppBundle\Mailer``. One way to do this is with an expression: +to another service: ``App\Mailer``. One way to do this is with an expression: .. configuration-block:: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # ... - AppBundle\Mail\MailerConfiguration: ~ + App\Mail\MailerConfiguration: ~ - AppBundle\Mailer: + App\Mailer: # the '@=' prefix is required when using expressions for arguments in YAML files - arguments: ["@=service('AppBundle\\\\Mail\\\\MailerConfiguration').getMailerMethod()"] + arguments: ['@=service("App\\Mail\\MailerConfiguration").getMailerMethod()'] + # when using double-quoted strings, the backslash needs to be escaped twice (see https://yaml.org/spec/1.2/spec.html#id2787109) + # arguments: ["@=service('App\\\\Mail\\\\MailerConfiguration').getMailerMethod()"] .. code-block:: xml - + - + - + service('App\\Mail\\MailerConfiguration').getMailerMethod() @@ -53,15 +55,21 @@ to another service: ``AppBundle\Mailer``. One way to do this is with an expressi .. code-block:: php - // app/config/services.php - use AppBundle\Mail\MailerConfiguration; - use AppBundle\Mailer; - use Symfony\Component\ExpressionLanguage\Expression; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - $container->autowire(MailerConfiguration::class); + use App\Mail\MailerConfiguration; + use App\Mailer; + + return function(ContainerConfigurator $configurator) { + // ... + + $services->set(MailerConfiguration::class); + + $services->set(Mailer::class) + ->args([expr(sprintf('service(%s).getMailerMethod()', MailerConfiguration::class))]); + }; - $container->autowire(Mailer::class) - ->addArgument(new Expression('service("AppBundle\\\\Mail\\\\MailerConfiguration").getMailerMethod()')); To learn more about the expression language syntax, see :doc:`/components/expression_language/syntax`. @@ -79,13 +87,15 @@ via a ``container`` variable. Here's another example: .. code-block:: yaml + # config/services.yaml services: - AppBundle\Mailer: + App\Mailer: # the '@=' prefix is required when using expressions for arguments in YAML files arguments: ["@=container.hasParameter('some_param') ? parameter('some_param') : 'default_value'"] .. code-block:: xml + - + container.hasParameter('some_param') ? parameter('some_param') : 'default_value' @@ -101,13 +111,17 @@ via a ``container`` variable. Here's another example: .. code-block:: php - use AppBundle\Mailer; - use Symfony\Component\ExpressionLanguage\Expression; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Mailer; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); - $container->autowire(Mailer::class) - ->addArgument(new Expression( - "container.hasParameter('some_param') ? parameter('some_param') : 'default_value'" - )); + $services->set(Mailer::class) + ->args([expr("container.hasParameter('some_param') ? parameter('some_param') : 'default_value'")]); + }; Expressions can be used in ``arguments``, ``properties``, as arguments with ``configurator`` and as arguments to ``calls`` (method calls). diff --git a/service_container/factories.rst b/service_container/factories.rst index 3595786346c..52b843eb88a 100644 --- a/service_container/factories.rst +++ b/service_container/factories.rst @@ -4,13 +4,17 @@ 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. +Symfony's Service Container provides multiple features to control the creation +of objects, allowing you to specify arguments passed to the constructor as well +as calling methods and setting parameters. + +However, sometimes you need to apply the `factory design pattern`_ to delegate +the object creation to some special object called "the factory". In those cases, +the service container can call a method on your factory to create the object +rather than directly instantiating the class. + +Static Factories +---------------- Suppose you have a factory that configures and returns a new ``NewsletterManager`` object by calling the static ``createNewsletterManager()`` method:: @@ -27,27 +31,25 @@ object by calling the static ``createNewsletterManager()`` method:: } } -To make the ``NewsletterManager`` object available as a service, you can -configure the service container to use the -``NewsletterManagerStaticFactory::createNewsletterManager()`` factory method: +To make the ``NewsletterManager`` object available as a service, use the +``factory`` option to define which method of which class must be called to +create its object: .. configuration-block:: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # ... - AppBundle\Email\NewsletterManager: - # call the static method that creates the object - factory: ['AppBundle\Email\NewsletterManagerStaticFactory', 'createNewsletterManager'] - # define the class of the created object - class: AppBundle\Email\NewsletterManager + App\Email\NewsletterManager: + # the first argument is the class and the second argument is the static method + factory: ['App\Email\NewsletterManagerStaticFactory', 'createNewsletterManager'] .. code-block:: xml - + - - - + + + - @@ -71,14 +72,20 @@ configure the service container to use the .. code-block:: php - // app/config/services.php - use AppBundle\Email\NewsletterManager; - use AppBundle\Email\NewsletterManagerStaticFactory; - // ... + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Email\NewsletterManager; + use App\Email\NewsletterManagerStaticFactory; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(NewsletterManager::class) + // the first argument is the class and the second argument is the static method + ->factory([NewsletterManagerStaticFactory::class, 'createNewsletterManager']); + }; - $container->register(NewsletterManager::class, NewsletterManager::class) - // call the static method - ->setFactory([NewsletterManagerStaticFactory::class, 'createNewsletterManager']); .. note:: @@ -88,31 +95,32 @@ configure the service container to use the the configured class name may be used by compiler passes and therefore should be set to a sensible value. -If your factory is not using a static function to configure and create your -service, but a regular method, you can instantiate the factory itself as a -service too. Later, in the ":ref:`factories-passing-arguments-factory-method`" -section, you learn how you can inject arguments in this method. +Non-Static Factories +-------------------- +If your factory is using a regular method instead of a static one to configure +and create the service, instantiate the factory itself as a service too. Configuration of the service container then looks like this: .. configuration-block:: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # ... - AppBundle\Email\NewsletterManagerFactory: ~ + # first, create a service for the factory + App\Email\NewsletterManagerFactory: ~ - AppBundle\Email\NewsletterManager: - # call a method on the specified factory service - factory: ['@AppBundle\Email\NewsletterManagerFactory', 'createNewsletterManager'] - class: AppBundle\Email\NewsletterManager + # second, use the factory service as the first argument of the 'factory' + # option and the factory method as the second argument + App\Email\NewsletterManager: + factory: ['@App\Email\NewsletterManagerFactory', 'createNewsletterManager'] .. code-block:: xml - + - - - + + - - + + @@ -135,20 +143,94 @@ Configuration of the service container then looks like this: .. code-block:: php - // app/config/services.php - use AppBundle\Email\NewsletterManager; - use AppBundle\Email\NewsletterManagerFactory; - use Symfony\Component\DependencyInjection\Reference; - // ... + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Email\NewsletterManager; + use App\Email\NewsletterManagerFactory; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + // first, create a service for the factory + $services->set(NewsletterManagerFactory::class); + + // second, use the factory service as the first argument of the 'factory' + // method and the factory method as the second argument + $services->set(NewsletterManager::class) + ->factory([ref(NewsletterManagerFactory::class), 'createNewsletterManager']); + }; + +.. _factories-invokable: + +Invokable Factories +------------------- + +Suppose you now change your factory method to ``__invoke()`` so that your +factory service can be used as a callback:: + + class InvokableNewsletterManagerFactory + { + public function __invoke() + { + $newsletterManager = new NewsletterManager(); + + // ... + + return $newsletterManager; + } + } - $container->register(NewsletterManagerFactory::class); +Services can be created and configured via invokable factories by omitting the +method name, just as routes can reference +:ref:`invokable controllers `. - $container->register(NewsletterManager::class, NewsletterManager::class) - // call a method on the specified factory service - ->setFactory([ - new Reference(NewsletterManagerFactory::class), - 'createNewsletterManager', - ]); +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\Email\NewsletterManager: + class: App\Email\NewsletterManager + factory: '@App\Email\NewsletterManagerFactory' + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Email\NewsletterManager; + use App\Email\NewsletterManagerFactory; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(NewsletterManager::class) + ->args([ref('templating')]) + ->factory(ref(NewsletterManagerFactory::class)); + }; .. _factories-passing-arguments-factory-method: @@ -161,25 +243,24 @@ Passing Arguments to the Factory Method that's enabled for your service. If you need to pass arguments to the factory method you can use the ``arguments`` -options. For example, suppose the ``createNewsletterManager()`` method in the previous -example takes the ``templating`` service as an argument: +option. For example, suppose the ``createNewsletterManager()`` method in the +previous examples takes the ``templating`` service as an argument: .. configuration-block:: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # ... - AppBundle\Email\NewsletterManager: - class: AppBundle\Email\NewsletterManager - factory: 'AppBundle\Email\NewsletterManagerFactory:createNewsletterManager' + App\Email\NewsletterManager: + factory: ['@App\Email\NewsletterManagerFactory', createNewsletterManager] arguments: ['@templating'] .. code-block:: xml - + - - + + @@ -199,15 +279,19 @@ example takes the ``templating`` service as an argument: .. code-block:: php - // app/config/services.php - use AppBundle\Email\NewsletterManager; - use AppBundle\Email\NewsletterManagerFactory; - use Symfony\Component\DependencyInjection\Reference; - - // ... - $container->register(NewsletterManager::class, NewsletterManager::class) - ->addArgument(new Reference('templating')) - ->setFactory([ - new Reference(NewsletterManagerFactory::class), - 'createNewsletterManager', - ]); + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Email\NewsletterManager; + use App\Email\NewsletterManagerFactory; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(NewsletterManager::class) + ->factory([ref(NewsletterManagerFactory::class), 'createNewsletterManager']) + ->args([ref('templating')]) + ; + }; + +.. _`factory design pattern`: https://en.wikipedia.org/wiki/Factory_(object-oriented_programming) diff --git a/service_container/import.rst b/service_container/import.rst index 8679a06c91f..40ad01d42ed 100644 --- a/service_container/import.rst +++ b/service_container/import.rst @@ -13,10 +13,8 @@ How to Import Configuration Files/Resources 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. +(``config/services.yaml`` by default). This gives you absolute flexibility over +the services in your application. External service configuration can be imported in two different ways. The first method, commonly used to import other resources, is via the ``imports`` @@ -32,15 +30,15 @@ methods. Importing Configuration with ``imports`` ---------------------------------------- -By default, service configuration lives in ``app/config/services.yml``. But if that -file becomes large, you're free to organize into multiple files. For suppose you +By default, service configuration lives in ``config/services.yaml``. But if that +file becomes large, you're free to organize into multiple files. Suppose you decided to move some configuration to a new file: .. configuration-block:: .. code-block:: yaml - # app/config/services/mailer.yml + # config/services/mailer.yaml parameters: # ... some parameters @@ -49,7 +47,7 @@ decided to move some configuration to a new file: .. code-block:: xml - + + + + + + + + .. code-block:: php - // app/config/services.php - $loader->import('services/mailer.php'); + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $configurator) { + $configurator->import('services/mailer.php'); + + $services = $configurator->services() + ->defaults() + ->autowire() + ->autoconfigure() + ->private() + ; -The ``resource`` location, for files, is either a relative path from the -current file or an absolute path. + $services->load('App\\', '../src/*') + ->exclude('../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'); + }; + +When loading a configuration file, Symfony loads first the imported files and +then it processes the parameters and services defined in the file. If you use the +:ref:`default services.yaml configuration ` +as in the above example, the ``App\`` definition creates services for classes +found in ``../src/*``. If your imported file defines services for those classes +too, they will be overridden. + +A possible solution for this is to add the classes and/or directories of the +imported files in the ``exclude`` option of the ``App\`` definition. Another +solution is to not use imports and add the service definitions in the same file, +but after the ``App\`` definition to override it. .. include:: /components/dependency_injection/_imports-parameters-note.rst.inc @@ -115,7 +156,7 @@ Importing Configuration via Container Extensions ------------------------------------------------ Third-party bundle container configuration, including Symfony core services, -are usually loaded using another method: a container extension. +are usually loaded using another method: a :doc:`container extension `. Internally, each bundle defines its services in files like you've seen so far. However, these files aren't imported using the ``import`` directive. Instead, bundles @@ -123,10 +164,6 @@ use a *dependency injection extension* to load the files automatically. As soon as you enable a bundle, its extension is called, which is able to load service configuration files. -In fact, each configuration block in ``config.yml`` - e.g. ``framework`` or ``twig``- -is passed to the extension for that bundle - e.g. ``FrameworkBundle`` or ``TwigBundle`` - +In fact, each configuration file in ``config/packages/`` is passed to the +extension of its related bundle - e.g. ``FrameworkBundle`` or ``TwigBundle`` - and used to configure those services further. - -If you want to use dependency injection extensions in your own shared -bundles and provide user friendly configuration, take a look at the -:doc:`/bundles/extension` article. diff --git a/service_container/injection_types.rst b/service_container/injection_types.rst index 370876346dc..cc42c1cdf32 100644 --- a/service_container/injection_types.rst +++ b/service_container/injection_types.rst @@ -19,7 +19,7 @@ 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:: - namespace AppBundle\Mail; + namespace App\Mail; // ... class NewsletterManager @@ -41,14 +41,16 @@ service container configuration: .. code-block:: yaml + # config/services.yaml services: # ... - AppBundle\Mail\NewsletterManager: + App\Mail\NewsletterManager: arguments: ['@mailer'] .. code-block:: xml + - + @@ -66,12 +68,18 @@ service container configuration: .. code-block:: php - use AppBundle\Mail\NewsletterManager; - use Symfony\Component\DependencyInjection\Reference; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Mail\NewsletterManager; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(NewsletterManager::class) + ->args(ref('mailer')); + }; - // ... - $container->register('app.newsletter_manager', NewsletterManager::class) - ->addArgument(new Reference('mailer')); .. tip:: @@ -97,6 +105,108 @@ 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. +Immutable-setter Injection +-------------------------- + +Another possible injection is to use a method which returns a separate instance +by cloning the original service, this approach allows you to make a service immutable:: + + // ... + use Symfony\Component\Mailer\MailerInterface; + + class NewsletterManager + { + private $mailer; + + /** + * @required + * @return static + */ + public function withMailer(MailerInterface $mailer) + { + $new = clone $this; + $new->mailer = $mailer; + + return $new; + } + + // ... + } + +In order to use this type of injection, don't forget to configure it: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + app.newsletter_manager: + class: App\Mail\NewsletterManager + calls: + - [withMailer, ['@mailer'], true] + + .. code-block:: xml + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + use App\Mail\NewsletterManager; + use Symfony\Component\DependencyInjection\Reference; + + // ... + $container->register('app.newsletter_manager', NewsletterManager::class) + ->addMethodCall('withMailer', [new Reference('mailer')], true); + +.. note:: + + If you decide to use autowiring, this type of injection requires + that you add a ``@return static`` docblock in order for the container + to be capable of registering the method. + +This approach is useful if you need to configure your service according to your needs, +so, here's the advantages of immutable-setters: + +* Immutable setters works with optional dependencies, this way, if you don't need + a dependency, the setter don't need to be called. + +* Like the constructor injection, using immutable setters force the dependency to stay + the same during the lifetime of a service. + +* This type of injection works well with traits as the service can be composed, + this way, adapting the service to your application requirements is easier. + +* The setter can be called multiple times, this way, adding a dependency to a collection + becomes easier and allows you to add a variable number of dependencies. + +The disadvantages are: + +* As the setter call is optional, a dependency can be null during execution, + you must check that the dependency is available before calling it. + +* Unless the service is declared lazy, it is incompatible with services + that reference each other in what are called circular loops. + Setter Injection ---------------- @@ -120,16 +230,18 @@ that accepts the dependency:: .. code-block:: yaml + # config/services.yaml services: # ... app.newsletter_manager: - class: AppBundle\Mail\NewsletterManager + class: App\Mail\NewsletterManager calls: - [setMailer, ['@mailer']] .. code-block:: xml + - + @@ -149,22 +261,30 @@ that accepts the dependency:: .. code-block:: php - use AppBundle\Mail\NewsletterManager; - use Symfony\Component\DependencyInjection\Reference; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - // ... - $container->register('app.newsletter_manager', NewsletterManager::class) - ->addMethodCall('setMailer', [new Reference('mailer')]); + use App\Mail\NewsletterManager; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(NewsletterManager::class) + ->call('setMailer', [ref('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. + need the dependency, then 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. +* Like the immutable-setter one, this type of injection works well with + traits and allows you to compose your service. + The disadvantages of setter injection are: * The setter can be called more than just at the time of construction so @@ -178,7 +298,7 @@ The disadvantages of setter injection are: Property Injection ------------------ -Another possibility is just setting public fields of the class directly:: +Another possibility is setting public fields of the class directly:: // ... class NewsletterManager @@ -192,16 +312,18 @@ Another possibility is just setting public fields of the class directly:: .. code-block:: yaml + # config/services.yaml services: # ... app.newsletter_manager: - class: AppBundle\Mail\NewsletterManager + class: App\Mail\NewsletterManager properties: mailer: '@mailer' .. code-block:: xml + - + @@ -219,12 +341,17 @@ Another possibility is just setting public fields of the class directly:: .. code-block:: php - use AppBundle\Mail\NewsletterManager; - use Symfony\Component\DependencyInjection\Reference; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - // ... - $container->register('newsletter_manager', NewsletterManager::class) - ->setProperty('mailer', new Reference('mailer')); + use App\Mail\NewsletterManager; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set('app.newsletter_manager', NewsletterManager::class) + ->property('mailer', ref('mailer')); + }; There are mainly only disadvantages to using property injection, it is similar to setter injection but with these additional important problems: diff --git a/service_container/lazy_services.rst b/service_container/lazy_services.rst index 44dec8552c4..00867948966 100644 --- a/service_container/lazy_services.rst +++ b/service_container/lazy_services.rst @@ -26,21 +26,12 @@ 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 ``ocramius/proxy-manager`` package: +In order to use the lazy service instantiation, you will need to install the +``symfony/proxy-manager-bridge`` package: .. code-block:: terminal - $ composer require ocramius/proxy-manager - -.. note:: - - If you're not using the full-stack framework, you also have to install the - `ProxyManager bridge`_ - - .. code-block:: terminal - - $ composer require symfony/proxy-manager-bridge + $ composer require symfony/proxy-manager-bridge Configuration ------------- @@ -51,14 +42,14 @@ You can mark the service as ``lazy`` by manipulating its definition: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: - AppBundle\Twig\AppExtension: - lazy: true + App\Twig\AppExtension: + lazy: true .. code-block:: xml - + - + .. code-block:: php - // app/config/services.php - use AppBundle\Twig\AppExtension; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Twig\AppExtension; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(AppExtension::class)->lazy(); + }; - $container->register(AppExtension::class) - ->setLazy(true); Once you inject the service into another service, a virtual `proxy`_ with the same signature of the class representing the service should be injected. The @@ -93,8 +90,8 @@ To check if your proxy works you can check the interface of the received object: .. note:: If you don't install the `ProxyManager bridge`_ and the - `ocramius/proxy-manager`_, the container will just skip over the ``lazy`` - flag and instantiate the service as it would normally do. + `ocramius/proxy-manager`_, the container will skip over the ``lazy`` + flag and directly instantiate the service as it would normally do. Additional Resources -------------------- diff --git a/service_container/optional_dependencies.rst b/service_container/optional_dependencies.rst index 40cdd6126b1..3593934c5c3 100644 --- a/service_container/optional_dependencies.rst +++ b/service_container/optional_dependencies.rst @@ -15,7 +15,7 @@ if the service does not exist: .. code-block:: xml - + - + @@ -33,18 +33,18 @@ if the service does not exist: .. code-block:: php - // app/config/services.php - use AppBundle\Newsletter\NewsletterManager; - use Symfony\Component\DependencyInjection\ContainerInterface; - use Symfony\Component\DependencyInjection\Reference; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - // ... + use App\Newsletter\NewsletterManager; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(NewsletterManager::class) + ->args([ref('logger')->nullOnInvalid()]); + }; - $container->register(NewsletterManager::class) - ->addArgument(new Reference( - 'logger', - ContainerInterface::NULL_ON_INVALID_REFERENCE - )); .. note:: @@ -64,16 +64,15 @@ call if the service exists and remove the method call if it does not: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: - app.newsletter_manager: - class: AppBundle\Newsletter\NewsletterManager + App\Newsletter\NewsletterManager: calls: - [setLogger, ['@?logger']] .. code-block:: xml - + - + @@ -91,20 +90,18 @@ call if the service exists and remove the method call if it does not: .. code-block:: php - // app/config/services.php - use AppBundle\Newsletter\NewsletterManager; - use Symfony\Component\DependencyInjection\ContainerInterface; - use Symfony\Component\DependencyInjection\Reference; - - $container - ->register(NewsletterManager::class) - ->addMethodCall('setLogger', [ - new Reference( - 'logger', - ContainerInterface::IGNORE_ON_INVALID_REFERENCE - ), - ]) - ; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Newsletter\NewsletterManager; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(NewsletterManager::class) + ->call('setLogger', [ref('logger')->ignoreOnInvalid()]) + ; + }; .. note:: diff --git a/service_container/parameters.rst b/service_container/parameters.rst deleted file mode 100644 index f99077c4981..00000000000 --- a/service_container/parameters.rst +++ /dev/null @@ -1,310 +0,0 @@ -.. index:: - single: DependencyInjection; Parameters - -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. - -Parameters in Configuration Files ---------------------------------- - -Use the ``parameters`` section of a config file to set parameters: - -.. configuration-block:: - - .. code-block:: yaml - - parameters: - mailer.transport: sendmail - - .. code-block:: xml - - - - - - sendmail - - - - .. code-block:: php - - $container->setParameter('mailer.transport', 'sendmail'); - -You can refer to parameters elsewhere in any config file 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: - -.. configuration-block:: - - .. code-block:: yaml - - parameters: - mailer.transport: sendmail - - services: - AppBundle\Service\Mailer: - arguments: ['%mailer.transport%'] - - .. code-block:: xml - - - - - - sendmail - - - - - %mailer.transport% - - - - - .. code-block:: php - - use AppBundle\Mailer; - - $container->setParameter('mailer.transport', 'sendmail'); - - $container->register(Mailer::class) - ->addArgument('%mailer.transport%'); - -.. caution:: - - 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``: - - .. code-block:: xml - - - sendmail - - - 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: - - .. code-block:: xml - - sendmail - -.. note:: - - If you use a string that starts with ``@`` or has ``%`` anywhere in it, you - need to escape it by adding another ``@`` or ``%``: - - .. configuration-block:: - - .. code-block:: yaml - - # app/config/parameters.yml - parameters: - # This will be parsed as string '@securepass' - mailer_password: '@@securepass' - - # Parsed as http://symfony.com/?foo=%s&bar=%d - url_pattern: 'http://symfony.com/?foo=%%s&bar=%%d' - - .. code-block:: xml - - - - @securepass - - - http://symfony.com/?foo=%%s&bar=%%d - - - .. code-block:: php - - // the @ symbol does NOT need to be escaped in PHP - $container->setParameter('mailer_password', '@securepass'); - - // But % does need to be escaped - $container->setParameter('url_pattern', 'http://symfony.com/?foo=%%s&bar=%%d'); - -Getting and Setting Container Parameters in PHP ------------------------------------------------ - -Working with container parameters is straightforward using the container's -accessor methods for parameters:: - - // checks if a parameter is defined - $container->hasParameter('mailer.transport'); - - // gets value of a parameter - $container->getParameter('mailer.transport'); - - // adds a new parameter - $container->setParameter('mailer.transport', 'sendmail'); - -.. caution:: - - The used ``.`` notation is just a - :ref:`Symfony convention ` to make parameters - easier to read. Parameters are just flat key-value elements, they can't - be organized into a nested array - -.. note:: - - You can only set a parameter before the container is compiled: not at run-time. - To learn more about compiling the container see - :doc:`/components/dependency_injection/compilation`. - -.. versionadded:: 3.4 - - Container parameters are case sensitive starting from Symfony 3.4. In - previous Symfony versions, parameters were case insensitive, meaning that - ``mailer.transport`` and ``Mailer.Transport`` were considered the same parameter. - -.. _component-di-parameters-array: - -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. - -.. configuration-block:: - - .. code-block:: yaml - - parameters: - my_mailer.gateways: [mail1, mail2, mail3] - - my_multilang.language_fallback: - en: - - en - - fr - fr: - - fr - - en - - .. code-block:: xml - - - - - - - mail1 - mail2 - mail3 - - - - - en - fr - - - - fr - en - - - - - - .. code-block:: php - - $container->setParameter('my_mailer.gateways', ['mail1', 'mail2', 'mail3']); - $container->setParameter('my_multilang.language_fallback', [ - 'en' => ['en', 'fr'], - 'fr' => ['fr', 'en'], - ]); - -Environment Variables and Dynamic Values ----------------------------------------- - -See :doc:`/configuration/external_parameters`. - -.. _component-di-parameters-constants: - -Constants as Parameters ------------------------ - -Setting PHP constants as parameters is also supported: - -.. configuration-block:: - - .. code-block:: yaml - - parameters: - global.constant.value: !php/const GLOBAL_CONSTANT - my_class.constant.value: !php/const My_Class::CONSTANT_NAME - - .. code-block:: xml - - - - - - GLOBAL_CONSTANT - My_Class::CONSTANT_NAME - - - - .. code-block:: php - - $container->setParameter('global.constant.value', GLOBAL_CONSTANT); - $container->setParameter('my_class.constant.value', My_Class::CONSTANT_NAME); - -PHP Keywords in XML -------------------- - -By default, ``true``, ``false`` and ``null`` in XML are converted to the -PHP keywords (respectively ``true``, ``false`` and ``null``): - -.. code-block:: xml - - - false - - - - -To disable this behavior, use the ``string`` type: - -.. code-block:: xml - - - true - - - - -.. note:: - - This is not available for YAML and PHP, because they already have built-in - support for the PHP keywords. diff --git a/service_container/parent_services.rst b/service_container/parent_services.rst index 31fd839a807..1e1f6846860 100644 --- a/service_container/parent_services.rst +++ b/service_container/parent_services.rst @@ -9,8 +9,8 @@ have related classes that share some of the same dependencies. For example, you may have multiple repository classes which need the ``doctrine.orm.entity_manager`` service and an optional ``logger`` service:: - // src/AppBundle/Repository/BaseDoctrineRepository.php - namespace AppBundle\Repository; + // src/Repository/BaseDoctrineRepository.php + namespace App\Repository; use Doctrine\Common\Persistence\ObjectManager; use Psr\Log\LoggerInterface; @@ -36,10 +36,10 @@ you may have multiple repository classes which need the Your child service classes may look like this:: - // src/AppBundle/Repository/DoctrineUserRepository.php - namespace AppBundle\Repository; + // src/Repository/DoctrineUserRepository.php + namespace App\Repository; - use AppBundle\Repository\BaseDoctrineRepository; + use App\Repository\BaseDoctrineRepository; // ... class DoctrineUserRepository extends BaseDoctrineRepository @@ -47,10 +47,10 @@ Your child service classes may look like this:: // ... } - // src/AppBundle/Repository/DoctrinePostRepository.php - namespace AppBundle\Repository; + // src/Repository/DoctrinePostRepository.php + namespace App\Repository; - use AppBundle\Repository\BaseDoctrineRepository; + use App\Repository\BaseDoctrineRepository; // ... class DoctrinePostRepository extends BaseDoctrineRepository @@ -66,24 +66,26 @@ duplicated service definitions: .. code-block:: yaml + # config/services.yaml services: - AppBundle\Repository\BaseDoctrineRepository: + App\Repository\BaseDoctrineRepository: abstract: true arguments: ['@doctrine.orm.entity_manager'] calls: - [setLogger, ['@logger']] - AppBundle\Repository\DoctrineUserRepository: - # extend the AppBundle\Repository\BaseDoctrineRepository service - parent: AppBundle\Repository\BaseDoctrineRepository + App\Repository\DoctrineUserRepository: + # extend the App\Repository\BaseDoctrineRepository service + parent: App\Repository\BaseDoctrineRepository - AppBundle\Repository\DoctrinePostRepository: - parent: AppBundle\Repository\BaseDoctrineRepository + App\Repository\DoctrinePostRepository: + parent: App\Repository\BaseDoctrineRepository # ... .. code-block:: xml + - + @@ -99,13 +101,13 @@ duplicated service definitions: - - + - @@ -114,33 +116,36 @@ duplicated service definitions: .. code-block:: php - use AppBundle\Repository\BaseDoctrineRepository; - use AppBundle\Repository\DoctrinePostRepository; - use AppBundle\Repository\DoctrineUserRepository; - use Symfony\Component\DependencyInjection\ChildDefinition; - use Symfony\Component\DependencyInjection\Reference; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - $container->register(BaseDoctrineRepository::class) - ->setAbstract(true) - ->addArgument(new Reference('doctrine.orm.entity_manager')) - ->addMethodCall('setLogger', [new Reference('logger')]) - ; + use App\Repository\BaseDoctrineRepository; + use App\Repository\DoctrinePostRepository; + use App\Repository\DoctrineUserRepository; - // extend the AppBundle\Repository\BaseDoctrineRepository service - $definition = new ChildDefinition(BaseDoctrineRepository::class); - $definition->setClass(DoctrineUserRepository::class); - $container->setDefinition(DoctrineUserRepository::class, $definition); + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); - $definition = new ChildDefinition(BaseDoctrineRepository::class); - $definition->setClass(DoctrinePostRepository::class); - $container->setDefinition(DoctrinePostRepository::class, $definition); + $services->set(BaseDoctrineRepository::class) + ->abstract() + ->args([ref('doctrine.orm.entity_manager')]) + ->call('setLogger', [ref('logger')]) + ; - // ... + $services->set(DoctrineUserRepository::class) + // extend the App\Repository\BaseDoctrineRepository service + ->parent(BaseDoctrineRepository::class) + ; + + $services->set(DoctrinePostRepository::class) + ->parent(BaseDoctrineRepository::class) + ; + }; 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 ``EntityManager`` will be injected and ``setLogger()`` will -be called when ``AppBundle\Repository\DoctrineUserRepository`` is instantiated. +be called when ``App\Repository\DoctrineUserRepository`` is instantiated. All attributes on the parent service are shared with the child **except** for ``shared``, ``abstract`` and ``tags``. These are *not* inherited from the parent. @@ -155,7 +160,7 @@ All attributes on the parent service are shared with the child **except** for 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 + You can also extract common parts of similar service definitions into a parent service without also extending a parent class in PHP. Overriding Parent Dependencies @@ -169,11 +174,12 @@ the child class: .. code-block:: yaml + # config/services.yaml services: # ... - AppBundle\Repository\DoctrineUserRepository: - parent: AppBundle\Repository\BaseDoctrineRepository + App\Repository\DoctrineUserRepository: + parent: App\Repository\BaseDoctrineRepository # overrides the public setting of the parent service public: false @@ -182,8 +188,8 @@ the child class: # argument list arguments: ['@app.username_checker'] - AppBundle\Repository\DoctrinePostRepository: - parent: AppBundle\Repository\BaseDoctrineRepository + App\Repository\DoctrinePostRepository: + parent: App\Repository\BaseDoctrineRepository # overrides the first argument (using the special index_N key) arguments: @@ -191,6 +197,7 @@ the child class: .. code-block:: xml + - @@ -223,23 +230,36 @@ the child class: .. code-block:: php - use AppBundle\Repository\BaseDoctrineRepository; - use AppBundle\Repository\DoctrinePostRepository; - use AppBundle\Repository\DoctrineUserRepository; - use Symfony\Component\DependencyInjection\ChildDefinition; - use Symfony\Component\DependencyInjection\Reference; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Repository\BaseDoctrineRepository; + use App\Repository\DoctrinePostRepository; + use App\Repository\DoctrineUserRepository; // ... - $definition = new ChildDefinition(BaseDoctrineRepository::class); - $definition->setClass(DoctrineUserRepository::class); - // overrides the public setting of the parent service - $definition->setPublic(false); - // appends the '@app.username_checker' argument to the parent argument list - $definition->addArgument(new Reference('app.username_checker')); - $container->setDefinition(DoctrineUserRepository::class, $definition); - - $definition = new ChildDefinition(BaseDoctrineRepository::class); - $definition->setClass(DoctrinePostRepository::class); - // overrides the first argument - $definition->replaceArgument(0, new Reference('doctrine.custom_entity_manager')); - $container->setDefinition(DoctrinePostRepository::class, $definition); + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(BaseDoctrineRepository::class) + // ... + ; + + $services->set(DoctrineUserRepository::class) + ->parent(BaseDoctrineRepository::class) + + // overrides the public setting of the parent service + ->private() + + // appends the '@app.username_checker' argument to the parent + // argument list + ->args([ref('app.username_checker')]) + ; + + $services->set(DoctrinePostRepository::class) + ->parent(BaseDoctrineRepository::class) + + # overrides the first argument + ->arg(0, ref('doctrine.custom_entity_manager')) + ; + }; diff --git a/service_container/request.rst b/service_container/request.rst index 5b4f4af738c..10637e7feac 100644 --- a/service_container/request.rst +++ b/service_container/request.rst @@ -11,7 +11,7 @@ add it as an argument to the methods that need the request or inject the :method:`Symfony\\Component\\HttpFoundation\\RequestStack::getCurrentRequest` method:: - namespace AppBundle\Newsletter; + namespace App\Newsletter; use Symfony\Component\HttpFoundation\RequestStack; @@ -33,8 +33,8 @@ method:: // ... } -Now, just inject the ``request_stack``, which behaves like any normal service. -If you're using the :ref:`default services.yml configuration `, +Now, inject the ``request_stack``, which behaves like any normal service. +If you're using the :ref:`default services.yaml configuration `, this will happen automatically via autowiring. .. tip:: diff --git a/service_container/service_decoration.rst b/service_container/service_decoration.rst index c9de48976d9..5d070345f6f 100644 --- a/service_container/service_decoration.rst +++ b/service_container/service_decoration.rst @@ -12,12 +12,12 @@ When overriding an existing definition, the original service is lost: # config/services.yaml services: - AppBundle\Mailer: ~ + App\Mailer: ~ - # this replaces the old AppBundle\Mailer definition with the new one, the + # this replaces the old App\Mailer definition with the new one, the # old definition is lost - AppBundle\Mailer: - class: AppBundle\NewMailer + App\Mailer: + class: App\NewMailer .. code-block:: xml @@ -28,25 +28,31 @@ When overriding an existing definition, the original service is lost: xsd:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> - + - - + .. code-block:: php // config/services.php - use AppBundle\Mailer; - use AppBundle\NewMailer; + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - $container->register(Mailer::class); + use App\Mailer; + use App\NewMailer; - // this replaces the old AppBundle\Mailer definition with the new one, the - // old definition is lost - $container->register(Mailer::class, NewMailer::class); + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(Mailer::class); + + // this replaces the old App\Mailer definition with the new one, the + // old definition is lost + $services->set(Mailer::class, NewMailer::class); + }; Most of the time, that's exactly what you want to do. But sometimes, you might want to decorate the old one instead (i.e. apply the `Decorator pattern`_). @@ -60,18 +66,73 @@ but keeps a reference of the old one as ``App\DecoratingMailer.inner``: # config/services.yaml services: - AppBundle\Mailer: ~ + App\Mailer: ~ - AppBundle\DecoratingMailer: - # overrides the AppBundle\Mailer service - # but that service is still available as AppBundle\DecoratingMailer.inner - decorates: AppBundle\Mailer + App\DecoratingMailer: + # overrides the App\Mailer service + # but that service is still available as App\DecoratingMailer.inner + decorates: App\Mailer - # pass the old service as an argument - arguments: ['@AppBundle\DecoratingMailer.inner'] + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\DecoratingMailer; + use App\Mailer; - # private, because usually you do not need to fetch AppBundle\DecoratingMailer directly - public: false + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(Mailer::class); + + $services->set(DecoratingMailer::class) + // overrides the App\Mailer service + // but that service is still available as App\DecoratingMailer.inner + ->decorate(Mailer::class); + }; + +The ``decorates`` option tells the container that the ``App\DecoratingMailer`` +service replaces the ``App\Mailer`` service. If you're using the +:ref:`default services.yaml configuration `, +the decorated service is automatically injected when the constructor of the +decorating service has one argument type-hinted with the decorated service class. + +If you are not using autowiring or the decorating service has more than one +constructor argument type-hinted with the decorated service class, you must +inject the decorated service explicitly (the ID of the decorated service is +automatically changed to ``decorating_service_id + '.inner'``): + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + App\Mailer: ~ + + App\DecoratingMailer: + decorates: App\Mailer + # pass the old service as an argument + arguments: ['@App\DecoratingMailer.inner'] .. code-block:: xml @@ -82,13 +143,12 @@ but keeps a reference of the old one as ``App\DecoratingMailer.inner``: xsd:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> - + - - + @@ -97,32 +157,33 @@ but keeps a reference of the old one as ``App\DecoratingMailer.inner``: .. code-block:: php // config/services.php - use AppBundle\DecoratingMailer; - use AppBundle\Mailer; - use Symfony\Component\DependencyInjection\Reference; + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\DecoratingMailer; + use App\Mailer; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); - $container->register(Mailer::class); + $services->set(Mailer::class); - $container->register(DecoratingMailer::class) - ->setDecoratedService(Mailer::class) - ->addArgument(new Reference(DecoratingMailer::class.'.inner')) - ->setPublic(false) - ; + $services->set(DecoratingMailer::class) + ->decorate(Mailer::class) + // pass the old service as an argument + ->args([ref(DecoratingMailer::class.'.inner')]); + }; -The ``decorates`` option tells the container that the ``AppBundle\DecoratingMailer`` service -replaces the ``AppBundle\Mailer`` service. The old ``AppBundle\Mailer`` service is renamed to -``AppBundle\DecoratingMailer.inner`` so you can inject it into your new service. .. tip:: - The visibility (public) of the decorated ``AppBundle\Mailer`` service (which is an alias - for the new service) will still be the same as the original ``AppBundle\Mailer`` + The visibility of the decorated ``App\Mailer`` service (which is an alias + for the new service) will still be the same as the original ``App\Mailer`` visibility. .. note:: The generated inner id is based on the id of the decorator service - (``AppBundle\DecoratingMailer`` here), not of the decorated service (``AppBundle\Mailer`` + (``App\DecoratingMailer`` here), not of the decorated service (``App\Mailer`` here). You can control the inner service name via the ``decoration_inner_name`` option: @@ -132,10 +193,10 @@ replaces the ``AppBundle\Mailer`` service. The old ``AppBundle\Mailer`` service # config/services.yaml services: - AppBundle\DecoratingMailer: + App\DecoratingMailer: # ... - decoration_inner_name: AppBundle\DecoratingMailer.wooz - arguments: ['@AppBundle\DecoratingMailer.wooz'] + decoration_inner_name: App\DecoratingMailer.wooz + arguments: ['@App\DecoratingMailer.wooz'] .. code-block:: xml @@ -149,12 +210,12 @@ replaces the ``AppBundle\Mailer`` service. The old ``AppBundle\Mailer`` service - + @@ -163,14 +224,20 @@ replaces the ``AppBundle\Mailer`` service. The old ``AppBundle\Mailer`` service .. code-block:: php // config/services.php - use AppBundle\DecoratingMailer; - use Symfony\Component\DependencyInjection\Reference; + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - $container->register(DecoratingMailer::class) - ->setDecoratedService(AppBundle\Mailer, DecoratingMailer::class.'.wooz') - ->addArgument(new Reference(DecoratingMailer::class.'.wooz')) - // ... - ; + use App\DecoratingMailer; + use App\Mailer; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(Mailer::class); + + $services->set(DecoratingMailer::class) + ->decorate(Mailer::class, DecoratingMailer::class.'.wooz') + ->args([ref(DecoratingMailer::class.'.wooz')]); + }; Decoration Priority ------------------- @@ -223,19 +290,24 @@ the ``decoration_priority`` option. Its value is an integer that defaults to .. code-block:: php // config/services.php - use Symfony\Component\DependencyInjection\Reference; + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(Foo::class); - $container->register(Foo::class) + $services->set(Bar::class) + ->private() + ->decorate(Foo::class, null, 5) + ->args([ref(Bar::class.'.inner')]); - $container->register(Bar::class) - ->addArgument(new Reference(Bar::class.'.inner')) - ->setPublic(false) - ->setDecoratedService(Foo::class, null, 5); + $services->set(Baz::class) + ->private() + ->decorate(Foo::class, null, 1) + ->args([ref(Baz::class.'.inner')]); + }; - $container->register(Baz::class) - ->addArgument(new Reference(Baz::class.'.inner')) - ->setPublic(false) - ->setDecoratedService(Foo::class, null, 1); The generated code will be the following:: diff --git a/service_container/service_subscribers_locators.rst b/service_container/service_subscribers_locators.rst index 617c896e51c..e178932f53e 100644 --- a/service_container/service_subscribers_locators.rst +++ b/service_container/service_subscribers_locators.rst @@ -4,10 +4,6 @@ Service Subscribers & Locators ============================== -.. versionadded:: 3.3 - - Service subscribers and locators were introduced in Symfony 3.3. - Sometimes, a service needs access to several other services without being sure that all of them will actually be used. In those cases, you may want the instantiation of the services to be lazy. However, that's not possible using @@ -20,8 +16,8 @@ Another example are applications that implement the `Command pattern`_ using a CommandBus to map command handlers by Command class names and use them to handle their respective command when it is asked for:: - // src/AppBundle/CommandBus.php - namespace AppBundle; + // src/CommandBus.php + namespace App; // ... class CommandBus @@ -72,13 +68,13 @@ Use its ``getSubscribedServices()`` method to include as many services as needed in the service subscriber and change the type hint of the container to a PSR-11 ``ContainerInterface``:: - // src/AppBundle/CommandBus.php - namespace AppBundle; + // src/CommandBus.php + namespace App; - use AppBundle\CommandHandler\BarHandler; - use AppBundle\CommandHandler\FooHandler; + use App\CommandHandler\BarHandler; + use App\CommandHandler\FooHandler; use Psr\Container\ContainerInterface; - use Symfony\Component\DependencyInjection\ServiceSubscriberInterface; + use Symfony\Contracts\Service\ServiceSubscriberInterface; class CommandBus implements ServiceSubscriberInterface { @@ -92,8 +88,8 @@ a PSR-11 ``ContainerInterface``:: public static function getSubscribedServices() { return [ - 'AppBundle\FooCommand' => FooHandler::class, - 'AppBundle\BarCommand' => BarHandler::class, + 'App\FooCommand' => FooHandler::class, + 'App\BarCommand' => BarHandler::class, ]; } @@ -203,15 +199,15 @@ service type to a service. .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: - AppBundle\CommandBus: + App\CommandBus: tags: - { name: 'container.service_subscriber', key: 'logger', id: 'monolog.logger.event' } .. code-block:: xml - + - + @@ -228,15 +224,17 @@ service type to a service. .. code-block:: php - // app/config/services.php - use AppBundle\CommandBus; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - // ... + use App\CommandBus; - $container - ->register(CommandBus::class) - ->addTag('container.service_subscriber', ['key' => 'logger', 'id' => 'monolog.logger.event']) - ; + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(CommandBus::class) + ->tag('container.service_subscriber', ['key' => 'logger', 'id' => 'monolog.logger.event']); + }; .. tip:: @@ -247,26 +245,35 @@ Defining a Service Locator -------------------------- To manually define a service locator, create a new service definition and add -the ``container.service_locator`` tag to it. Use its ``arguments`` option to -include as many services as needed in it. +the ``container.service_locator`` tag to it. Use the first argument of the +service definition to pass a collection of services to the service locator: .. configuration-block:: .. code-block:: yaml - // app/config/services.yml + # config/services.yaml services: app.command_handler_locator: class: Symfony\Component\DependencyInjection\ServiceLocator - tags: ['container.service_locator'] arguments: - - AppBundle\FooCommand: '@app.command_handler.foo' - AppBundle\BarCommand: '@app.command_handler.bar' + App\FooCommand: '@app.command_handler.foo' + App\BarCommand: '@app.command_handler.bar' + # if you are not using the default service autoconfiguration, + # add the following tag to the service definition: + # tags: ['container.service_locator'] + + # if the element has no key, the ID of the original service is used + app.another_command_handler_locator: + class: Symfony\Component\DependencyInjection\ServiceLocator + arguments: + - + - '@app.command_handler.baz' .. code-block:: xml - + - - + + + + - + @@ -287,20 +300,31 @@ include as many services as needed in it. .. code-block:: php - // app/config/services.php - use Symfony\Component\DependencyInjection\Reference; - use Symfony\Component\DependencyInjection\ServiceLocator; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - // ... + use Symfony\Component\DependencyInjection\ServiceLocator; - $container - ->register('app.command_handler_locator', ServiceLocator::class) - ->addTag('container.service_locator') - ->setArguments([[ - 'AppBundle\FooCommand' => new Reference('app.command_handler.foo'), - 'AppBundle\BarCommand' => new Reference('app.command_handler.bar'), - ]]) - ; + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set('app.command_handler_locator', ServiceLocator::class) + ->args([[ + 'App\FooCommand' => ref('app.command_handler.foo'), + 'App\BarCommand' => ref('app.command_handler.bar'), + ]]) + // if you are not using the default service autoconfiguration, + // add the following tag to the service definition: + // ->tag('container.service_locator') + ; + + // if the element has no key, the ID of the original service is used + $services->set('app.another_command_handler_locator', ServiceLocator::class) + ->args([[ + ref('app.command_handler.baz'), + ]]) + ; + }; .. note:: @@ -313,14 +337,14 @@ Now you can use the service locator by injecting it in any other service: .. code-block:: yaml - // app/config/services.yml + # config/services.yaml services: - AppBundle\CommandBus: + App\CommandBus: arguments: ['@app.command_handler_locator'] .. code-block:: xml - + - + @@ -337,14 +361,17 @@ Now you can use the service locator by injecting it in any other service: .. code-block:: php - // app/config/services.php - use AppBundle\CommandBus; - use Symfony\Component\DependencyInjection\Reference; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\CommandBus; - $container - ->register(CommandBus::class) - ->setArguments([new Reference('app.command_handler_locator')]) - ; + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(CommandBus::class) + ->args([ref('app.command_handler_locator')]); + }; In :doc:`compiler passes ` it's recommended to use the :method:`Symfony\\Component\\DependencyInjection\\Compiler\\ServiceLocatorTagPass::register` @@ -366,4 +393,250 @@ will share identical locators among all the services referencing them:: $myService->addArgument(ServiceLocatorTagPass::register($container, $locateableServices)); } +Indexing the Collection of Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Services passed to the service locator can define their own index using an +arbitrary attribute whose name is defined as ``index_by`` in the service locator. + +In the following example, the ``App\Handler\HandlerCollection`` locator receives +all services tagged with ``app.handler`` and they are indexed using the value +of the ``key`` tag attribute (as defined in the ``index_by`` locator option): + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + App\Handler\One: + tags: + - { name: 'app.handler', key: 'handler_one' } + + App\Handler\Two: + tags: + - { name: 'app.handler', key: 'handler_two' } + + App\HandlerCollection: + # inject all services tagged with app.handler as first argument + arguments: [!tagged_locator { tag: 'app.handler', index_by: 'key' }] + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(App\Handler\One::class) + ->tag('app.handler', ['key' => 'handler_one']) + ; + + $services->set(App\Handler\Two::class) + ->tag('app.handler', ['key' => 'handler_two']) + ; + + $services->set(App\Handler\HandlerCollection::class) + // inject all services tagged with app.handler as first argument + ->args([tagged_locator('app.handler', 'key')]) + ; + }; + +Inside this locator you can retrieve services by index using the value of the +``key`` attribute. For example, to get the ``App\Handler\Two`` service:: + + // src/Handler/HandlerCollection.php + namespace App\Handler; + + use Symfony\Component\DependencyInjection\ServiceLocator; + + class HandlerCollection + { + public function __construct(ServiceLocator $locator) + { + $handlerTwo = $locator->get('handler_two'); + } + + // ... + } + +Instead of defining the index in the service definition, you can return its +value in a method called ``getDefaultIndexName()`` inside the class associated +to the service:: + + // src/Handler/One.php + namespace App\Handler; + + class One + { + public static function getDefaultIndexName(): string + { + return 'handler_one'; + } + + // ... + } + +If you prefer to use another method name, add a ``default_index_method`` +attribute to the locator service defining the name of this custom method: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\HandlerCollection: + arguments: [!tagged_locator { tag: 'app.handler', default_index_method: 'myOwnMethodName' }] + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $configurator) { + $configurator->services() + ->set(App\HandlerCollection::class) + ->args([tagged_locator('app.handler', null, 'myOwnMethodName')]) + ; + }; + +Service Subscriber Trait +------------------------ + +The :class:`Symfony\\Contracts\\Service\\ServiceSubscriberTrait` provides an +implementation for :class:`Symfony\\Contracts\\Service\\ServiceSubscriberInterface` +that looks through all methods in your class that have no arguments and a return +type. It provides a ``ServiceLocator`` for the services of those return types. +The service id is ``__METHOD__``. This allows you to add dependencies to your +services based on type-hinted helper methods:: + + // src/Service/MyService.php + namespace App\Service; + + use Psr\Log\LoggerInterface; + use Symfony\Component\Routing\RouterInterface; + use Symfony\Contracts\Service\ServiceSubscriberInterface; + use Symfony\Contracts\Service\ServiceSubscriberTrait; + + class MyService implements ServiceSubscriberInterface + { + use ServiceSubscriberTrait; + + public function doSomething() + { + // $this->router() ... + // $this->logger() ... + } + + private function router(): RouterInterface + { + return $this->container->get(__METHOD__); + } + + private function logger(): LoggerInterface + { + return $this->container->get(__METHOD__); + } + } + +This allows you to create helper traits like RouterAware, LoggerAware, etc... +and compose your services with them:: + + // src/Service/LoggerAware.php + namespace App\Service; + + use Psr\Log\LoggerInterface; + + trait LoggerAware + { + private function logger(): LoggerInterface + { + return $this->container->get(__CLASS__.'::'.__FUNCTION__); + } + } + + // src/Service/RouterAware.php + namespace App\Service; + + use Symfony\Component\Routing\RouterInterface; + + trait RouterAware + { + private function router(): RouterInterface + { + return $this->container->get(__CLASS__.'::'.__FUNCTION__); + } + } + + // src/Service/MyService.php + namespace App\Service; + + use Symfony\Contracts\Service\ServiceSubscriberInterface; + use Symfony\Contracts\Service\ServiceSubscriberTrait; + + class MyService implements ServiceSubscriberInterface + { + use ServiceSubscriberTrait, LoggerAware, RouterAware; + + public function doSomething() + { + // $this->router() ... + // $this->logger() ... + } + } + +.. caution:: + + When creating these helper traits, the service id cannot be ``__METHOD__`` + as this will include the trait name, not the class name. Instead, use + ``__CLASS__.'::'.__FUNCTION__`` as the service id. + .. _`Command pattern`: https://en.wikipedia.org/wiki/Command_pattern diff --git a/service_container/shared.rst b/service_container/shared.rst index 49cbcd4dc5a..d676f592125 100644 --- a/service_container/shared.rst +++ b/service_container/shared.rst @@ -16,26 +16,32 @@ in your service definition: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: - AppBundle\SomeNonSharedService: + App\SomeNonSharedService: shared: false # ... .. code-block:: xml - + - + .. code-block:: php - // app/config/services.php - use AppBundle\SomeNonSharedService; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - $container->register(SomeNonSharedService::class) - ->setShared(false); + use App\SomeNonSharedService; -Now, whenever you request the ``AppBundle\SomeNonSharedService`` from the container, + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(SomeNonSharedService::class) + ->share(false); + }; + +Now, whenever you request the ``App\SomeNonSharedService`` from the container, you will be passed a new instance. diff --git a/service_container/synthetic_services.rst b/service_container/synthetic_services.rst index 59bb6fd3cd2..5a3ea59d276 100644 --- a/service_container/synthetic_services.rst +++ b/service_container/synthetic_services.rst @@ -38,14 +38,15 @@ configuration: .. code-block:: yaml + # config/services.yaml services: - # synthetic services don't specify a class app.synthetic_service: synthetic: true .. code-block:: xml + register('app.synthetic_service') - ->setSynthetic(true) - ; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + // synthetic services don't specify a class + $services->set('app.synthetic_service') + ->synthetic(); + }; + Now, you can inject the instance in the container using :method:`Container::set() `:: diff --git a/service_container/tags.rst b/service_container/tags.rst index a52153d323a..7484dc73826 100644 --- a/service_container/tags.rst +++ b/service_container/tags.rst @@ -13,15 +13,15 @@ example: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: - AppBundle\Twig\AppExtension: + App\Twig\AppExtension: public: false tags: ['twig.extension'] .. code-block:: xml - + - + @@ -37,12 +37,19 @@ example: .. code-block:: php - // app/config/services.php - use AppBundle\Twig\AppExtension; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Twig\AppExtension; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(AppExtension::class) + ->private() + ->tag('twig.extension'); + }; - $container->register(AppExtension::class) - ->setPublic(false) - ->addTag('twig.extension'); Services tagged with the ``twig.extension`` tag are collected during the initialization of TwigBundle and added to Twig as extensions. @@ -58,9 +65,9 @@ learn how to create your own custom tags, keep reading. Autoconfiguring Tags -------------------- -Starting in Symfony 3.3, if you enable :ref:`autoconfigure `, -then some tags are automatically applied for you. That's true for the ``twig.extension`` -tag: the container sees that your class extends ``AbstractExtension`` (or more accurately, +If you enable :ref:`autoconfigure `, then some tags are +automatically applied for you. That's true for the ``twig.extension`` tag: the +container sees that your class extends ``AbstractExtension`` (or more accurately, that it implements ``ExtensionInterface``) and adds the tag for you. If you want to apply tags automatically for your own services, use the @@ -70,22 +77,23 @@ If you want to apply tags automatically for your own services, use the .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: # this config only applies to the services created by this file _instanceof: # services whose classes are instances of CustomInterface will be tagged automatically - AppBundle\Security\CustomInterface: + App\Security\CustomInterface: tags: ['app.custom_tag'] # ... .. code-block:: xml + - + @@ -94,20 +102,28 @@ If you want to apply tags automatically for your own services, use the .. code-block:: php - use AppBundle\Security\CustomInterface; - // ... + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Security\CustomInterface; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + // this config only applies to the services created by this file + $services + ->instanceof(CustomInterface::class) + // services whose classes are instances of CustomInterface will be tagged automatically + ->tag('app.custom_tag'); + }; - // services whose classes are instances of CustomInterface will be tagged automatically - $container->registerForAutoconfiguration(CustomInterface::class) - ->addTag('app.custom_tag') - ->setAutowired(true); For more advanced needs, you can define the automatic tags using the :method:`Symfony\\Component\\DependencyInjection\\ContainerBuilder::registerForAutoconfiguration` method in an :doc:`extension ` or from your kernel:: - // app/AppKernel.php - class AppKernel extends Kernel + // src/Kernel.php + class Kernel extends BaseKernel { // ... @@ -135,8 +151,8 @@ ways of transporting the message until one succeeds. To begin with, define the ``TransportChain`` class:: - // src/AppBundle/Mail/TransportChain.php - namespace AppBundle\Mail; + // src/Mail/TransportChain.php + namespace App\Mail; class TransportChain { @@ -159,11 +175,13 @@ Then, define the chain as a service: .. code-block:: yaml + # config/services.yaml services: - AppBundle\Mail\TransportChain: ~ + App\Mail\TransportChain: ~ .. code-block:: xml + - + .. code-block:: php - use AppBundle\Mail\TransportChain; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Mail\TransportChain; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(TransportChain::class); + }; - $container->autowire(TransportChain::class); Define Services with a Custom Tag ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -192,6 +218,7 @@ For example, you may add the following transports as services: .. code-block:: yaml + # config/services.yaml services: Swift_SmtpTransport: arguments: ['%mailer_host%'] @@ -202,6 +229,7 @@ For example, you may add the following transports as services: .. code-block:: xml + register(\Swift_SmtpTransport::class) - ->addArgument('%mailer_host%') - ->addTag('app.mail_transport'); + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(\Swift_SmtpTransport::class) + ->args(['%mailer_host%']) + ->tag('app.mail_transport') + ; - $container->register(\Swift_SendmailTransport::class) - ->addTag('app.mail_transport'); + $services->set(\Swift_SendmailTransport::class) + ->tag('app.mail_transport') + ; + }; Notice that each service was given a tag named ``app.mail_transport``. This is the custom tag that you'll use in your compiler pass. The compiler pass is what @@ -242,10 +279,10 @@ Create a Compiler Pass You can now use a :ref:`compiler pass ` to ask the container for any services with the ``app.mail_transport`` tag:: - // src/AppBundle/DependencyInjection/Compiler/MailTransportPass.php - namespace AppBundle\DependencyInjection\Compiler; + // src/DependencyInjection/Compiler/MailTransportPass.php + namespace App\DependencyInjection\Compiler; - use AppBundle\Mail\TransportChain; + use App\Mail\TransportChain; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -275,18 +312,21 @@ Register the Pass with the Container ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In order to run the compiler pass when the container is compiled, you have to -add the compiler pass to the container in the ``build()`` method of your -bundle:: +add the compiler pass to the container in a :doc:`bundle extension ` +or from your kernel:: - // src/AppBundle/AppBundle.php + // src/Kernel.php + namespace App; + use App\DependencyInjection\Compiler\MailTransportPass; + use Symfony\Component\HttpKernel\Kernel as BaseKernel; // ... - use AppBundle\DependencyInjection\Compiler\MailTransportPass; - use Symfony\Component\DependencyInjection\ContainerBuilder; - class AppBundle extends Bundle + class Kernel extends BaseKernel { - public function build(ContainerBuilder $container) + // ... + + protected function build(ContainerBuilder $container) { $container->addCompilerPass(new MailTransportPass()); } @@ -340,6 +380,7 @@ To answer this, change the service declaration: .. code-block:: yaml + # config/services.yaml services: Swift_SmtpTransport: arguments: ['%mailer_host%'] @@ -352,6 +393,7 @@ To answer this, change the service declaration: .. code-block:: xml + register(\Swift_SmtpTransport::class) - ->addArgument('%mailer_host%') - ->addTag('app.mail_transport', ['alias' => 'smtp']); + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(\Swift_SmtpTransport::class) + ->args(['%mailer_host%']) + ->tag('app.mail_transport', ['alias' => 'smtp']) + ; - $container->register(\Swift_SendmailTransport::class) - ->addTag('app.mail_transport', ['alias' => 'sendmail']); + $services->set(\Swift_SendmailTransport::class) + ->tag('app.mail_transport', ['alias' => 'sendmail']) + ; + }; .. tip:: @@ -388,8 +439,8 @@ To answer this, change the service declaration: .. code-block:: yaml + # config/services.yaml services: - # Compact syntax Swift_SendmailTransport: class: \Swift_SendmailTransport @@ -401,11 +452,6 @@ To answer this, change the service declaration: tags: - { name: 'app.mail_transport' } - .. versionadded:: 3.3 - - Support for the compact tag notation in the YAML format was introduced - in Symfony 3.3. - Notice that you've added a generic ``alias`` key to the tag. To actually use this, update the compiler:: @@ -440,11 +486,6 @@ tags set for the current service and gives you the attributes. Reference Tagged Services ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 3.4 - - Support for the tagged service notation in YAML, XML and PHP was introduced - in Symfony 3.4. - Symfony provides a shortcut to inject all services tagged with a specific tag, which is a common need in some applications, so you don't have to write a compiler pass just for that. @@ -456,22 +497,22 @@ first constructor argument to the ``App\HandlerCollection`` service: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: - AppBundle\Handler\One: + App\Handler\One: tags: ['app.handler'] - AppBundle\Handler\Two: + App\Handler\Two: tags: ['app.handler'] - AppBundle\HandlerCollection: + App\HandlerCollection: # inject all services tagged with app.handler as first argument arguments: - - !tagged app.handler + - !tagged_iterator app.handler .. code-block:: xml - + - + - + - + - + .. code-block:: php - // app/config/services.php - use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - $container->register(AppBundle\Handler\One::class) - ->addTag('app.handler'); + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(App\Handler\One::class) + ->tag('app.handler') + ; - $container->register(AppBundle\Handler\Two::class) - ->addTag('app.handler'); + $services->set(App\Handler\Two::class) + ->tag('app.handler') + ; - $container->register(AppBundle\HandlerCollection::class) - // inject all services tagged with app.handler as first argument - ->addArgument(new TaggedIteratorArgument('app.handler')); + $services->set(App\HandlerCollection::class) + // inject all services tagged with app.handler as first argument + ->args([tagged_iterator('app.handler')]) + ; + }; After compilation the ``HandlerCollection`` service is able to iterate over your application handlers:: - // src/AppBundle/HandlerCollection.php - namespace AppBundle; + // src/HandlerCollection.php + namespace App; class HandlerCollection { @@ -530,15 +578,15 @@ application handlers:: .. code-block:: yaml - # app/config/services.yml + # config/services.yaml services: - AppBundle\Handler\One: + App\Handler\One: tags: - { name: 'app.handler', priority: 20 } .. code-block:: xml - + - + @@ -554,8 +602,15 @@ application handlers:: .. code-block:: php - // app/config/services.php - $container->register(AppBundle\Handler\One::class) - ->addTag('app.handler', ['priority' => 20]); + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(App\Handler\One::class) + ->tag('app.handler', ['priority' => 20]) + ; + }; Note that any other custom attributes will be ignored by this feature. diff --git a/session.rst b/session.rst index 144f068d34f..53cf435eb15 100644 --- a/session.rst +++ b/session.rst @@ -1,8 +1,227 @@ Sessions ======== +Symfony provides a session object and several utilities that you can use to +store information about the user between requests. + +Configuration +------------- + +Sessions are provided by the `HttpFoundation component`_, which is included in +all Symfony applications, no matter how you installed it. Before using the +sessions, check their default configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + session: + # enables the support of sessions in the app + enabled: true + # ID of the service used for session storage. + # NULL = means that PHP's default session mechanism is used + handler_id: null + # improves the security of the cookies used for sessions + cookie_secure: 'auto' + cookie_samesite: 'lax' + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + $container->loadFromExtension('framework', [ + 'session' => [ + // enables the support of sessions in the app + 'enabled' => true, + // ID of the service used for session storage + // NULL means that PHP's default session mechanism is used + 'handler_id' => null, + // improves the security of the cookies used for sessions + 'cookie_secure' => 'auto', + 'cookie_samesite' => 'lax', + ], + ]); + +Setting the ``handler_id`` config option to ``null`` means that Symfony will +use the native PHP session mechanism. The session metadata files will be stored +outside of the Symfony application, in a directory controlled by PHP. Although +this usually simplify things, some session expiration related options may not +work as expected if other applications that write to the same directory have +short max lifetime settings. + +If you prefer, you can use the ``session.handler.native_file`` service as +``handler_id`` to let Symfony manage the sessions itself. Another useful option +is ``save_path``, which defines the directory where Symfony will store the +session metadata files: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + session: + # ... + handler_id: 'session.handler.native_file' + save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%' + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + $container->loadFromExtension('framework', [ + 'session' => [ + // ... + 'handler_id' => 'session.handler.native_file', + 'save_path' => '%kernel.project_dir%/var/sessions/%kernel.environment%', + ], + ]); + +Check out the Symfony config reference to learn more about the other available +:ref:`Session configuration options `. Also, if you +prefer to store session metadata in a database instead of the filesystem, +check out this article: :doc:`/doctrine/pdo_session_storage`. + +Basic Usage +----------- + +Symfony provides a session service that is injected in your services and +controllers if you type-hint an argument with +:class:`Symfony\\Component\\HttpFoundation\\Session\\SessionInterface`:: + + use Symfony\Component\HttpFoundation\Session\SessionInterface; + + class SomeService + { + private $session; + + public function __construct(SessionInterface $session) + { + $this->session = $session; + } + + public function someMethod() + { + // stores an attribute in the session for later reuse + $this->session->set('attribute-name', 'attribute-value'); + + // gets an attribute by name + $foo = $this->session->get('foo'); + + // the second argument is the value returned when the attribute doesn't exist + $filters = $this->session->get('filters', []); + + // ... + } + } + +.. tip:: + + Every ``SessionInterface`` implementation is supported. If you have your + own implementation, type-hint this in the argument instead. + +Stored attributes remain in the session for the remainder of that user's session. +By default, session attributes are key-value pairs managed with the +:class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBag` +class. + +If your application needs are complex, you may prefer to use +:ref:`namespaced session attributes ` which are managed with the +:class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\NamespacedAttributeBag` +class. Before using them, override the ``session`` service definition to replace +the default ``AttributeBag`` by the ``NamespacedAttributeBag``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + session: + public: true + class: Symfony\Component\HttpFoundation\Session\Session + arguments: ['@session.storage', '@session.namespacedattributebag', '@session.flash_bag'] + + session.namespacedattributebag: + class: Symfony\Component\HttpFoundation\Session\Attribute\NamespacedAttributeBag + +.. _session-avoid-start: + +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 may hurt your application performance +because all users will receive a session cookie. In order to prevent that, you +must *completely* avoid accessing the session. + +For example, if your templates include some code to display the +:ref:`flash messages `, sessions will start even if the user +is not logged in and even if you haven't created any flash messages. To avoid +this behavior, add a check before trying to access the flash messages: + +.. code-block:: html+twig + + {# this check prevents starting a session when there are no flash messages #} + {% if app.request.hasPreviousSession %} + {% for message in app.flashes('notice') %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + +More about Sessions +------------------- + .. toctree:: :maxdepth: 1 - :glob: - session/* + /doctrine/pdo_session_storage + session/locale_sticky_session + session/php_bridge + session/proxy_examples + +.. _`HttpFoundation component`: https://symfony.com/components/HttpFoundation diff --git a/session/avoid_session_start.rst b/session/avoid_session_start.rst deleted file mode 100644 index b4222b280d5..00000000000 --- a/session/avoid_session_start.rst +++ /dev/null @@ -1,43 +0,0 @@ -.. index:: - single: Sessions, cookies - -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: - -.. code-block:: html+twig - - {% for message in app.flashes('notice') %} -
- {{ message }} -
- {% 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: - -.. code-block:: html+twig - - {% if app.request.hasPreviousSession %} - {% for message in app.flashes('notice') %} -
- {{ message }} -
- {% endfor %} - {% endif %} - -.. versionadded:: 3.3 - - The ``app.flashes()`` Twig function was introduced in Symfony 3.3. Prior, - you had to use ``app.session.flashBag()``. diff --git a/session/locale_sticky_session.rst b/session/locale_sticky_session.rst index 1f3bbe74e81..f8caef23370 100644 --- a/session/locale_sticky_session.rst +++ b/session/locale_sticky_session.rst @@ -17,11 +17,11 @@ Create a :ref:`new event subscriber `. Typically, ``_locale`` is used as a routing parameter to signify the locale, though you can determine the correct locale however you want:: - // src/AppBundle/EventSubscriber/LocaleSubscriber.php - namespace AppBundle\EventSubscriber; + // src/EventSubscriber/LocaleSubscriber.php + namespace App\EventSubscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\GetResponseEvent; + use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; class LocaleSubscriber implements EventSubscriberInterface @@ -33,7 +33,7 @@ correct locale however you want:: $this->defaultLocale = $defaultLocale; } - public function onKernelRequest(GetResponseEvent $event) + public function onKernelRequest(RequestEvent $event) { $request = $event->getRequest(); if (!$request->hasPreviousSession()) { @@ -58,7 +58,7 @@ correct locale however you want:: } } -If you're using the :ref:`default services.yml configuration `, +If you're using the :ref:`default services.yaml configuration `, you're done! Symfony will automatically know about the event subscriber and call the ``onKernelRequest`` method on each request. @@ -73,16 +73,18 @@ via some "Change Locale" route & controller), or create a route with the :ref:`_ .. code-block:: yaml + # config/services.yaml services: # ... - AppBundle\EventSubscriber\LocaleSubscriber: + App\EventSubscriber\LocaleSubscriber: arguments: ['%kernel.default_locale%'] - # redundant if you're using autoconfigure - tags: [kernel.event_subscriber] + # uncomment the next line if you are not using autoconfigure + # tags: [kernel.event_subscriber] .. code-block:: xml + - + %kernel.default_locale% - + + .. code-block:: php - use AppBundle\EventSubscriber\LocaleSubscriber; + // config/services.php + use App\EventSubscriber\LocaleSubscriber; $container->register(LocaleSubscriber::class) ->addArgument('%kernel.default_locale%') - ->addTag('kernel.event_subscriber'); + // uncomment the next line if you are not using autoconfigure + // ->addTag('kernel.event_subscriber'); That's it! Now celebrate by changing the user's locale and seeing that it's sticky throughout the request. @@ -115,7 +120,7 @@ method:: // from a controller... use Symfony\Component\HttpFoundation\Request; - public function indexAction(Request $request) + public function index(Request $request) { $locale = $request->getLocale(); } @@ -137,8 +142,8 @@ locale value before they are redirected to their first page. To do this, you need an event subscriber on the ``security.interactive_login`` event:: - // src/AppBundle/EventSubscriber/UserLocaleSubscriber.php - namespace AppBundle\EventSubscriber; + // src/EventSubscriber/UserLocaleSubscriber.php + namespace App\EventSubscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface; @@ -158,9 +163,6 @@ event:: $this->session = $session; } - /** - * @param InteractiveLoginEvent $event - */ public function onInteractiveLogin(InteractiveLoginEvent $event) { $user = $event->getAuthenticationToken()->getUser(); diff --git a/session/php_bridge.rst b/session/php_bridge.rst index 54502c3c681..42c8644e2a7 100644 --- a/session/php_bridge.rst +++ b/session/php_bridge.rst @@ -15,6 +15,7 @@ for the ``handler_id``: .. code-block:: yaml + # config/packages/framework.yaml framework: session: storage_id: session.storage.php_bridge @@ -22,6 +23,7 @@ for the ``handler_id``: .. code-block:: xml + loadFromExtension('framework', [ 'session' => [ 'storage_id' => 'session.storage.php_bridge', @@ -54,6 +57,7 @@ the example below: .. code-block:: yaml + # config/packages/framework.yaml framework: session: storage_id: session.storage.php_bridge @@ -61,6 +65,7 @@ the example below: .. code-block:: xml + loadFromExtension('framework', [ 'session' => [ 'storage_id' => 'session.storage.php_bridge', diff --git a/session/proxy_examples.rst b/session/proxy_examples.rst index e8b3d84b6a4..c4c3f9423a3 100644 --- a/session/proxy_examples.rst +++ b/session/proxy_examples.rst @@ -6,12 +6,12 @@ Session Proxy Examples The session proxy mechanism has a variety of uses and this article demonstrates two common uses. Rather than using the regular session handler, you can create -a custom save handler just by defining a class that extends the +a custom save handler by defining a class that extends the :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\SessionHandlerProxy` class. Then, define the class as a :ref:`service `. -If you're using the :ref:`default services.yml configuration `, +If you're using the :ref:`default services.yaml configuration `, that happens automatically. Finally, use the ``framework.session.handler_id`` configuration option to tell @@ -21,15 +21,15 @@ Symfony to use your session handler instead of the default one: .. code-block:: yaml - # app/config/config.yml + # config/packages/framework.yaml framework: session: # ... - handler_id: AppBundle\Session\CustomSessionHandler + handler_id: App\Session\CustomSessionHandler .. code-block:: xml - + - + .. code-block:: php - // app/config/config.php - use AppBundle\Session\CustomSessionHandler; + // config/packages/framework.php + use App\Session\CustomSessionHandler; $container->loadFromExtension('framework', [ // ... 'session' => [ @@ -65,8 +65,8 @@ If you want to encrypt the session data, you can use the proxy to encrypt and decrypt the session as required. The following example uses the `php-encryption`_ library, but you can adapt it to any other library that you may be using:: - // src/AppBundle/Session/EncryptedSessionProxy.php - namespace AppBundle\Session; + // src/Session/EncryptedSessionProxy.php + namespace App\Session; use Defuse\Crypto\Crypto; use Defuse\Crypto\Key; @@ -105,9 +105,10 @@ 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:: - // src/AppBundle/Session/ReadOnlySessionProxy.php - namespace AppBundle\Session; + // src/Session/ReadOnlySessionProxy.php + namespace App\Session; + use App\Entity\User; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; use Symfony\Component\Security\Core\Security; diff --git a/session/sessions_directory.rst b/session/sessions_directory.rst deleted file mode 100644 index 21a61d06818..00000000000 --- a/session/sessions_directory.rst +++ /dev/null @@ -1,54 +0,0 @@ -.. index:: - single: Sessions, sessions directory - -Configuring the Directory where Session Files are Saved -======================================================= - -By default, Symfony stores session metadata on the filesystem. If you want to control -this path, update the ``framework.session.save_path`` configuration key: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - session: - handler_id: session.handler.native_file - save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%' - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', [ - 'session' => [ - 'handler_id' => 'session.handler.native_file', - 'save_path' => '%kernel.project_dir%/var/sessions/%kernel.environment%' - ], - ]); - -Storing Sessions Elsewhere (e.g. database) ------------------------------------------- - -You can store your session data anywhere by using the ``handler_id`` option. -See :doc:`/components/http_foundation/session_configuration` for a discussion of -session save handlers. There are also articles about storing sessions in a -:doc:`relational database ` -or a :doc:`NoSQL database `. diff --git a/setup.rst b/setup.rst index 3b879a5441d..5b8a74f0b9c 100644 --- a/setup.rst +++ b/setup.rst @@ -4,304 +4,277 @@ Installing & Setting up the Symfony Framework ============================================= -This article explains how to install Symfony in different ways and how to solve -the most common issues that may appear during the installation process. - .. admonition:: Screencast :class: screencast - Do you prefer video tutorials? Check out the `Joyful Development with Symfony`_ + Do you prefer video tutorials? Check out the `Stellar Development with Symfony`_ screencast series. -Creating Symfony Applications ------------------------------ +.. _symfony-tech-requirements: -Symfony provides a dedicated application called the **Symfony Installer** to ease -the creation of Symfony applications. This installer is a PHP 5.4 compatible -executable that needs to be installed on your system only once: +Technical Requirements +---------------------- -**Linux and macOS systems**: +Before creating your first Symfony application you must: -.. class:: command-linux -.. code-block:: terminal +* Install PHP 7.2.5 or higher and these PHP extensions (which are installed and + enabled by default in most PHP 7 installations): `Ctype`_, `iconv`_, `JSON`_, + `PCRE`_, `Session`_, `SimpleXML`_, and `Tokenizer`_; +* `Install Composer`_, which is used to install PHP packages; +* `Install Symfony`_, which creates in your computer a binary called ``symfony`` + that provides all the tools you need to develop your application locally. - $ sudo mkdir -p /usr/local/bin - $ sudo curl -LsS https://symfony.com/installer -o /usr/local/bin/symfony - $ sudo chmod a+x /usr/local/bin/symfony +The ``symfony`` binary provides a tool to check if your computer meets these +requirements. Open your console terminal and run this command: -**Windows systems**: - -.. class:: command-windows .. code-block:: terminal - > php -r "file_put_contents('symfony', file_get_contents('https://symfony.com/installer'));" - -.. note:: + $ symfony check:requirements - In Linux and macOS, a global ``symfony`` command is created. In Windows, - move the ``symfony`` file to a directory that's included in the ``PATH`` - environment variable and create a ``symfony.bat`` file to create the global - command or move it to any other directory convenient for you: +.. _creating-symfony-applications: - .. class:: command-windows - .. code-block:: terminal - - # for example, if WAMP is used ... - > move symfony c:\wamp\bin\php - # create symfony.bat in the same folder - > cd c:\wamp\bin\php - > (echo @ECHO OFF & echo php "%~dp0symfony" %*) > symfony.bat - # ... then, execute the command as: - > symfony - - # moving it to your projects folder ... - > move symfony c:\projects - # ... then, execute the command as - > cd projects - > php symfony - -.. _installation-creating-the-app: +Creating Symfony Applications +----------------------------- -Once the Symfony Installer is installed, create your first Symfony application -with the ``new`` command: +Open your console terminal and run any of these commands to create a new Symfony +application: .. code-block:: terminal - $ symfony new my_project_name --version=3.4 + # run this if you are building a traditional web application + $ symfony new my_project_name --full -This command creates a new directory called ``my_project_name/`` that contains -an empty 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. + # run this if you are building a microservice, console application or API + $ symfony new my_project_name -.. note:: +The only difference between these two commands is the number of packages +installed by default. The ``--full`` option installs all the packages that you +usually need to build web applications, so the installation size will be bigger. - If the installer doesn't work for you or doesn't output anything, make sure - that the PHP `Phar extension`_ is installed and enabled on your computer. +If you can't or don't want to `install Symfony`_ for any reason, run these +commands to create the new Symfony application using Composer: -.. note:: +.. code-block:: terminal - If the SSL certificates are not properly installed in your system, you - may get this error: + # run this if you are building a traditional web application + $ composer create-project symfony/website-skeleton my_project_name - cURL error 60: SSL certificate problem: unable to get local issuer certificate. + # run this if you are building a microservice, console application or API + $ composer create-project symfony/skeleton my_project_name - You can solve this issue as follows: +No matter which command you run to create the Symfony application. All of them +will create a new ``my_project_name/`` directory, download some dependencies +into it and even generate the basic directories and files you'll need to get +started. In other words, your new application is ready! - #. Download a file with the updated list of certificates from - https://curl.haxx.se/ca/cacert.pem - #. Move the downloaded ``cacert.pem`` file to some safe location in your system - #. Update your ``php.ini`` file and configure the path to that file: +.. note:: - .. code-block:: ini + The project's cache and logs directory (by default, ``/var/cache/`` + and ``/var/log/``) must be writable by the web server. If you have + any issue, read how to :doc:`set up permissions for Symfony applications `. - ; Linux and macOS systems - curl.cainfo = "/path/to/cacert.pem" +Running Symfony Applications +---------------------------- - ; Windows systems - curl.cainfo = "C:\path\to\cacert.pem" +On production, you should use a web server like Nginx or Apache (see +:doc:`configuring a web server to run Symfony `). +But for development, it's more convenient to use the +:doc:`local web server ` provided by Symfony. -Basing your Project on a Specific Symfony Version -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This local server provides support for HTTP/2, TLS/SSL, automatic generation of +security certificates and many other features. It works with any PHP application, +not only Symfony projects, so it's a very useful development tool. -In case your project needs to be based on a specific Symfony version, use the -``version`` option of the ``new`` command: +Open your console terminal, move into your new project directory and start the +local web server as follows: .. code-block:: terminal - # use the most recent version in any Symfony branch - $ symfony new my_project_name --version=3.3 - $ symfony new my_project_name --version=3.4 + $ cd my-project/ + $ symfony server:start - # use the most recent 'lts' version (Long Term Support version) - $ symfony new my_project_name --version=lts +Open your browser and navigate to ``http://localhost:8000/``. If everything is +working, you'll see a welcome page. Later, when you are finished working, stop +the server by pressing ``Ctrl+C`` from your terminal. - # use the 'next' Symfony version to be released (still in development) - $ symfony new my_project_name --version=next +.. _install-existing-app: -Each version has its *own* documentation, which you can select on any documentation -page. +Setting up an Existing Symfony Project +-------------------------------------- -.. note:: +In addition to creating new Symfony projects, you will also work on projects +already created by other developers. In that case, you only need to get the +project code and install the dependencies with Composer. Assuming your team uses +Git, setup your project with the following commands: - Read the :doc:`Symfony Release process ` - to better understand why there are several Symfony versions and which one - to use for your projects. +.. code-block:: terminal -Creating Symfony Applications with Composer -------------------------------------------- + # clone the project to download its contents + $ cd projects/ + $ git clone ... -If you can't use the Symfony installer for any reason, you can create Symfony -applications with `Composer`_, the dependency manager used by modern PHP -applications. + # make Composer install the project's dependencies into vendor/ + $ cd my-project/ + $ composer install -If you don't have Composer installed in your computer, start by -`installing Composer`_. Then, execute the ``create-project`` command to create a -new Symfony application based on its latest stable version: +You'll probably also need to customize your :ref:`.env file ` +and do a few other project-specific tasks (e.g. creating a database). When +working on a existing Symfony application for the first time, it may be useful +to run this command which displays information about the project: .. code-block:: terminal - $ composer create-project symfony/framework-standard-edition my_project_name - -You can also install any other Symfony version by passing a second argument to -the ``create-project`` command: + $ php bin/console about -.. code-block:: terminal +.. _symfony-flex: - $ composer create-project symfony/framework-standard-edition my_project_name "2.8.*" +Installing Packages +------------------- -.. tip:: +A common practice when developing Symfony applications is to install packages +(Symfony calls them :doc:`bundles `) that provide ready-to-use +features. Packages usually require some setup before using them (editing some +file to enable the bundle, creating some file to add some initial config, etc.) - 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. +Most of the time this setup can be automated and that's why Symfony includes +`Symfony Flex`_, a tool to simplify the installation/removal of packages in +Symfony applications. Technically speaking, Symfony Flex is a Composer plugin +that is installed by default when creating a new Symfony application and which +**automates the most common tasks of Symfony applications**. -Running the Symfony Application -------------------------------- +.. tip:: -On production servers, Symfony applications use web servers such as Apache or -nginx (see :doc:`configuring a web server to run Symfony `). -However, on your local development machine you can also use the web server -provided by Symfony, which in turn uses the built-in web server provided by PHP. + You can also :doc:`add Symfony Flex to an existing project `. -First, :doc:`install the Symfony Web Server ` and -then, execute this command: +Symfony Flex modifies the behavior of the ``require``, ``update``, and +``remove`` Composer commands to provide advanced features. Consider the +following example: .. code-block:: terminal - $ cd my_project_name/ - $ php bin/console server:run + $ cd my-project/ + $ composer require logger -Open your browser and access the ``http://localhost:8000/`` URL to see the -Welcome Page of Symfony: +If you execute that command in a Symfony application which doesn't use Flex, +you'll see a Composer error explaining that ``logger`` is not a valid package +name. However, if the application has Symfony Flex installed, that command +installs and enables all the packages needed to use the official Symfony logger. -.. image:: /_images/quick_tour/welcome.png - :align: center - :alt: Symfony Welcome Page - :class: with-browser +.. _recipes-description: -If you see a blank page or an error page instead of the Welcome Page, there is -a directory permission misconfiguration. The solution to this problem is -explained in the :doc:`/setup/file_permissions`. +This is possible because lots of Symfony packages/bundles define **"recipes"**, +which are a set of automated instructions to install and enable packages into +Symfony applications. Flex keeps tracks of the recipes it installed in a +``symfony.lock`` file, which must be committed to your code repository. -When you are finished working on your Symfony application, stop the server by -pressing ``Ctrl+C`` from the terminal or command console. +Symfony Flex recipes are contributed by the community and they are stored in +two public repositories: -.. tip:: +* `Main recipe repository`_, is a curated list of recipes for high quality and + maintained packages. Symfony Flex only looks in this repository by default. - Symfony's web server is great for developing, but should **not** be - used on production. Instead, use Apache or nginx. - See :doc:`/setup/web_server_configuration`. +* `Contrib recipe repository`_, contains all the recipes created by the + community. All of them are guaranteed to work, but their associated packages + could be unmaintained. Symfony Flex will ask your permission before installing + any of these recipes. -Checking Symfony Application Configuration and Setup ----------------------------------------------------- +Read the `Symfony Recipes documentation`_ to learn everything about how to +create recipes for your own packages. -The Symfony Installer checks if your system is ready to run Symfony applications. -However, the PHP configuration for the command console can be different from the -PHP web configuration. For that reason, Symfony provides a visual configuration -checker. Access the following URL to check your configuration and fix any issue -before moving on: +.. _symfony-packs: -.. code-block:: text +Symfony Packs +~~~~~~~~~~~~~ - http://localhost:8000/config.php +Sometimes a single feature requires installing several packages and bundles. +Instead of installing them individually, Symfony provides **packs**, which are +Composer metapackages that include several dependencies. -Fixing Permissions Problems ---------------------------- +For example, to add debugging features in your application, you can run the +``composer require --dev debug`` command. This installs the ``symfony/debug-pack``, +which in turn installs several packages like ``symfony/debug-bundle``, +``symfony/monolog-bundle``, ``symfony/var-dumper``, etc. -If you have any file permission errors or see a white screen, then read -:doc:`/setup/file_permissions` for more information. +By default, when installing Symfony packs, your ``composer.json`` file shows the +pack dependency (e.g. ``"symfony/debug-pack": "^1.0"``) instead of the actual +packages installed. To show the packages, add the ``--unpack`` option when +installing a pack (e.g. ``composer require debug --dev --unpack``) or run this +command to unpack the already installed packs: ``composer unpack PACK_NAME`` +(e.g. ``composer unpack debug``). -.. _installation-updating-vendors: +.. _security-checker: -Updating Symfony Applications ------------------------------ +Checking Security Vulnerabilities +--------------------------------- -At this point, you've created a fully-functional Symfony application! Every Symfony -app depends on a number of third-party libraries stored in the ``vendor/`` directory -and managed by Composer. - -Updating those libraries frequently is a good practice to prevent bugs and -security vulnerabilities. Execute the ``update`` Composer command to update them -all at once (this can take up to several minutes to complete depending on the -complexity of your project): +The ``symfony`` binary created when you `install Symfony`_ provides a command to +check whether your project's dependencies contain any known security +vulnerability: .. code-block:: terminal - $ cd my_project_name/ - $ composer update + $ symfony check:security -.. tip:: +A good security practice is to execute this command regularly to be able to +update or replace compromised dependencies as soon as possible. The security +check is done locally by cloning the public `PHP security advisories database`_, +so your ``composer.lock`` file is not sent on the network. - Symfony provides a command to check whether your project's dependencies - contain any known security vulnerability: +.. tip:: - .. code-block:: terminal + The ``check:security`` command terminates with a non-zero exit code if + any of your dependencies is affected by a known security vulnerability. + This way you can add it to your project build process and your continuous + integration workflows to make them fail when there are vulnerabilities. - $ php bin/console security:check +Symfony LTS Versions +-------------------- - A good security practice is to execute this command regularly to be able to - update or replace compromised dependencies as soon as possible. +According to the :doc:`Symfony release process `, +"long-term support" (or LTS for short) versions are published every two years. +Check out the `Symfony releases`_ to know which is the latest LTS version. -.. _installing-a-symfony2-distribution: +By default, the command that creates new Symfony applications uses the latest +stable version. If you want to use an LTS version, add the ``--version`` option: -Installing the Symfony Demo or Other Distributions --------------------------------------------------- +.. code-block:: terminal -You've already downloaded the `Symfony Standard Edition`_: the default starting project -for all Symfony applications. You'll use this project throughout the documentation to -build your application! + # use the most recent LTS version + $ symfony new my_project_name --version=lts -Symfony also provides some other projects and starting skeletons that you can use: + # use the 'next' Symfony version to be released (still in development) + $ symfony new my_project_name --version=next -`The Symfony Demo Application`_ - This is a fully-functional application that shows the recommended way to develop - Symfony applications. The app has been conceived as a learning tool for Symfony - newcomers and its source code contains tons of comments and helpful notes. + # you can also select an exact specific Symfony version + $ symfony new my_project_name --version=4.4 -`The Symfony CMF Standard Edition`_ - The `Symfony CMF`_ is a project that helps make it easier for developers to add - CMS functionality to their Symfony applications. This is a starting project - containing the Symfony CMF. +The ``lts`` and ``next`` shortcuts are only available when using Symfony to +create new projects. If you use Composer, you need to tell the exact version: -`The Symfony REST Edition`_ - Shows how to build an application that provides a RESTful API using the - `FOSRestBundle`_ and several other related Bundles. +.. code-block:: terminal -.. _install-existing-app: + $ composer create-project symfony/website-skeleton:^4.4 my_project_name -Installing an Existing Symfony Application -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The Symfony Demo application +---------------------------- -When working collaboratively in a Symfony application, it's uncommon to create -a new Symfony application as explained in the previous sections. Instead, -someone else has already created and submitted it to a shared repository. +`The Symfony Demo Application`_ is a fully-functional application that shows the +recommended way to develop Symfony applications. It's a great learning tool for +Symfony newcomers and its code contains tons of comments and helpful notes. -It's recommended to not submit some files (:ref:`parameters.yml `) -and directories (``vendor/``, cache, logs) to the repository, so you'll have to do -the following when installing an existing Symfony application: +Run this command to create a new project based on the Symfony Demo application: .. code-block:: terminal - # clone the project to download its contents - $ cd projects/ - $ git clone ... - - # make Composer install the project's dependencies into vendor/ - $ cd my_project_name/ - $ composer install - - # now Composer will ask you for the values of any undefined parameter - $ ... + $ symfony new my_project_name --demo -Keep Going! ------------ +Start Coding! +------------- With setup behind you, it's time to :doc:`Create your first page in Symfony `. -Go Deeper with Setup --------------------- +Learn More +---------- .. toctree:: :hidden: @@ -313,18 +286,24 @@ Go Deeper with Setup :glob: setup/homestead - setup/new_project_git - setup/built_in_web_server setup/web_server_configuration setup/* -.. _`Joyful Development with Symfony`: https://symfonycasts.com/screencast/symfony3 -.. _`Composer`: https://getcomposer.org/ -.. _`installing Composer`: https://getcomposer.org/download/ -.. _`Phar extension`: https://php.net/manual/en/intro.phar.php -.. _`Symfony Standard Edition`: https://github.com/symfony/symfony-standard +.. _`Stellar Development with Symfony`: https://symfonycasts.com/screencast/symfony +.. _`Install Composer`: https://getcomposer.org/download/ +.. _`Install Symfony`: https://symfony.com/download +.. _`install Symfony`: https://symfony.com/download .. _`The Symfony Demo Application`: https://github.com/symfony/demo -.. _`The Symfony CMF Standard Edition`: https://github.com/symfony-cmf/standard-edition -.. _`Symfony CMF`: http://cmf.symfony.com/ -.. _`The Symfony REST Edition`: https://github.com/gimler/symfony-rest-edition -.. _`FOSRestBundle`: https://github.com/FriendsOfSymfony/FOSRestBundle +.. _`Symfony Flex`: https://github.com/symfony/flex +.. _`PHP security advisories database`: https://github.com/FriendsOfPHP/security-advisories +.. _`Symfony releases`: https://symfony.com/releases +.. _`Main recipe repository`: https://github.com/symfony/recipes +.. _`Contrib recipe repository`: https://github.com/symfony/recipes-contrib +.. _`Symfony Recipes documentation`: https://github.com/symfony/recipes/blob/master/README.rst +.. _`iconv`: https://php.net/book.iconv +.. _`JSON`: https://php.net/book.json +.. _`Session`: https://php.net/book.session +.. _`Ctype`: https://php.net/book.ctype +.. _`Tokenizer`: https://php.net/book.tokenizer +.. _`SimpleXML`: https://php.net/book.simplexml +.. _`PCRE`: https://php.net/book.pcre diff --git a/setup/_update_dep_errors.rst.inc b/setup/_update_dep_errors.rst.inc index ec84c75bb9f..49ae97067e4 100644 --- a/setup/_update_dep_errors.rst.inc +++ b/setup/_update_dep_errors.rst.inc @@ -1,16 +1,17 @@ Dependency Errors ~~~~~~~~~~~~~~~~~ -If you get a dependency error, it may simply mean that you need to upgrade -other Symfony dependencies too. In that case, try the following command: +If you get a dependency error, it may mean that you also need to upgrade +other libraries that are dependencies of the Symfony libraries. To allow +that, pass the ``--with-all-dependencies`` flag: .. code-block:: terminal - $ composer update symfony/symfony --with-dependencies + $ composer update "symfony/*" --with-all-dependencies -This updates ``symfony/symfony`` and *all* packages that it depends on, which will -include several other packages. By using tight version constraints in -``composer.json``, you can control what versions each library upgrades to. +This updates ``symfony/*`` and *all* packages that those packages depend on. +By using tight version constraints in ``composer.json``, you can control what +versions each library upgrades to. If this still doesn't work, your ``composer.json`` file may specify a version for a library that is not compatible with the newer Symfony version. In that diff --git a/setup/_vendor_deps.rst.inc b/setup/_vendor_deps.rst.inc index 064822f596d..58f582be681 100644 --- a/setup/_vendor_deps.rst.inc +++ b/setup/_vendor_deps.rst.inc @@ -11,8 +11,7 @@ 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 :ref:`Installation ` -article. +and you can read more about :doc:`installing Composer globally `. 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 @@ -34,7 +33,7 @@ To upgrade your libraries to new versions, run ``composer update``. 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 untracked files that are downloaded +of *your* repository. Instead, they're 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``, diff --git a/setup/built_in_web_server.rst b/setup/built_in_web_server.rst deleted file mode 100644 index 2e5a9f24156..00000000000 --- a/setup/built_in_web_server.rst +++ /dev/null @@ -1,149 +0,0 @@ -.. index:: - single: Web Server; Built-in Web Server - -How to Use PHP's built-in Web Server -==================================== - -The PHP 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 :doc:`Apache or nginx `. - -.. tip:: - - The preferred way to develop your Symfony application is to use - :doc:`Symfony Local Web Server `. - -.. caution:: - - The built-in web server is meant to be run in a controlled environment. - It is not designed to be used on public networks. - -Symfony provides a web server built on top of this PHP server to simplify your -local setup. This server is distributed as a bundle, so you must first install -and enable the server bundle. - -Installing the Web Server Bundle --------------------------------- - -First, execute this command: - -.. code-block:: terminal - - $ cd your-project/ - $ composer require --dev symfony/web-server-bundle - -Then, enable the bundle in the kernel of the application:: - - // app/AppKernel.php - class AppKernel extends Kernel - { - public function registerBundles() - { - $bundles = [ - // ... - ]; - - if ('dev' === $this->getEnvironment()) { - // ... - $bundles[] = new Symfony\Bundle\WebServerBundle\WebServerBundle(); - } - - // ... - } - - // ... - } - -Starting the Web Server ------------------------ - -Running a Symfony application using PHP's built-in web server is as easy as -executing the ``server:start`` command: - -.. code-block:: terminal - - $ php bin/console server:start - -This starts the web server at ``localhost:8000`` in the background that serves -your Symfony application. - -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: - -.. code-block:: terminal - - # passing a specific IP and port - $ php bin/console server:start 192.168.0.1:8080 - - # passing '*' as the IP means to use 0.0.0.0 (i.e. any local IP address) - $ php bin/console server:start *:8080 - -.. versionadded:: 3.4 - - The support of ``*`` as a valid IP address was introduced in Symfony 3.4. - -.. note:: - - You can use the ``server:status`` command to check if a web server is - listening: - - .. code-block:: terminal - - $ php bin/console server:status - -.. tip:: - - Some systems do not support the ``server:start`` command, in these cases - you can execute the ``server:run`` command. This command behaves slightly - different. Instead of starting the server in the background, it will block - the current terminal until you terminate it (this is usually done by - pressing Ctrl and C). - -.. sidebar:: Using the built-in Web Server from inside a Virtual Machine - - If you want to use the built-in web server from inside a virtual machine - and then load the site from a browser on your host machine, you'll need - to listen on the ``0.0.0.0:8000`` address (i.e. on all IP addresses that - are assigned to the virtual machine): - - .. code-block:: terminal - - $ php bin/console server:start 0.0.0.0:8000 - - .. caution:: - - You should **NEVER** listen to all interfaces on a computer that is - directly accessible from the Internet. The built-in web server is - not designed to be used on public networks. - -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 ``dev`` environment. -Use the ``--router`` option to use your own router script: - -.. code-block:: terminal - - $ php bin/console server:start --router=app/config/my_router.php - -If your application's document root differs from the standard directory layout, -you have to pass the correct location using the ``--docroot`` option: - -.. code-block:: terminal - - $ php bin/console server:start --docroot=public_html - -Stopping the Server -------------------- - -When you finish your work, you can stop the web server with the following command: - -.. code-block:: terminal - - $ php bin/console server:stop - -.. _`built-in web server`: https://php.net/manual/en/features.commandline.webserver.php -.. _`php.net`: https://php.net/manual/en/features.commandline.webserver.php#example-411 diff --git a/setup/bundles.rst b/setup/bundles.rst index ba346ecc7fe..53104c6c9ba 100644 --- a/setup/bundles.rst +++ b/setup/bundles.rst @@ -27,9 +27,9 @@ Most third-party bundles define their Symfony dependencies using the ``~2.N`` or } These constraints prevent the bundle from using Symfony 3 components, so it makes -it impossible to install it in a Symfony 3 based application. This issue is very -easy to solve thanks to the flexibility of Composer dependencies constraints. -Just replace ``~2.N`` by ``~2.N|~3.0`` (or ``^2.N`` by ``^2.N|~3.0``). +it impossible to install it in a Symfony 3 based application. Thanks to the +flexibility of Composer dependencies constraints, you can specify more than one +major version by replacing ``~2.N`` by ``~2.N|~3.0`` (or ``^2.N`` by ``^2.N|~3.0``). The above example can be updated to work with Symfony 3 as follows: @@ -69,7 +69,8 @@ PHPUnit test report: .. code-block:: terminal - $ phpunit + # this command is available after running "composer require --dev symfony/phpunit-bridge" + $ ./bin/phpunit # ... PHPUnit output @@ -112,7 +113,7 @@ in a Symfony 3 application. Assuming that you already have a Symfony 3 applicati you can test the updated bundle locally without having to install it through Composer. -If your operating system supports symbolic links, just point the appropriate +If your operating system supports symbolic links, instead point the appropriate vendor directory to your local bundle root directory: .. code-block:: terminal @@ -141,7 +142,7 @@ following recommended configuration as the starting point of your own configurat matrix: include: - php: 5.3.3 - env: COMPOSER_FLAGS='--prefer-lowest --prefer-stable' SYMFONY_DEPRECATIONS_HELPER=weak + env: COMPOSER_FLAGS='--prefer-lowest --prefer-stable' SYMFONY_DEPRECATIONS_HELPER=max[total]=999999 - php: 5.6 env: SYMFONY_VERSION='2.7.*' - php: 5.6 diff --git a/setup/file_permissions.rst b/setup/file_permissions.rst index 9a07fe06f85..4c65ac8325e 100644 --- a/setup/file_permissions.rst +++ b/setup/file_permissions.rst @@ -1,86 +1,19 @@ Setting up or Fixing File Permissions ===================================== -One important Symfony requirement is that the ``var`` directory must be -writable both by the web server and the command line user. - -On Linux and macOS systems, if your web server user is different from your -command line user, you need to configure permissions properly to avoid issues. -There are several ways to achieve that: - -1. Use the same User for the CLI and the Web Server -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Edit your web server configuration (commonly ``httpd.conf`` or ``apache2.conf`` -for Apache) and set its user to be the same as your CLI user (e.g. for Apache, -update the ``User`` and ``Group`` directives). - -.. caution:: - - If this solution is used in a production server, be sure this user only has - limited privileges (no access to private data or servers, execution of - unsafe binaries, etc.) as a compromised server would give to the hacker - those privileges. - -2. Using ACL on a System that Supports ``chmod +a`` (macOS) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -On macOS systems, the ``chmod`` command supports the ``+a`` flag to define an -ACL. Use the following script to determine your web server user and grant the -needed permissions: - -.. code-block:: terminal - - $ rm -rf var/cache/* - $ rm -rf var/logs/* - - $ HTTPDUSER=$(ps axo user,comm | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | head -1 | cut -d\ -f1) - $ sudo chmod +a "$HTTPDUSER allow delete,write,append,file_inherit,directory_inherit" var - $ sudo chmod +a "$(whoami) allow delete,write,append,file_inherit,directory_inherit" var - -3. Using ACL on a System that Supports ``setfacl`` (Linux/BSD) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Most Linux and BSD distributions don't support ``chmod +a``, but do support -another utility called ``setfacl``. You may need to install ``setfacl`` and -`enable ACL support`_ on your disk partition before using it. Then, use the -following script to determine your web server user and grant the needed permissions: - -.. code-block:: terminal - - $ HTTPDUSER=$(ps axo user,comm | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | head -1 | cut -d\ -f1) - # if this doesn't work, try adding `-n` option - $ sudo setfacl -dR -m u:"$HTTPDUSER":rwX -m u:$(whoami):rwX var - $ sudo setfacl -R -m u:"$HTTPDUSER":rwX -m u:$(whoami):rwX var - -.. note:: - - The first ``setfacl`` command sets permissions for future files and folders, - while the second one sets permissions on the existing files and folders. - Both of these commands assign permissions for the system user and the Apache - user. - - ``setfacl`` isn't available on NFS mount points. However, storing cache and - logs over NFS is strongly discouraged for performance reasons. - -4. Without Using ACL -~~~~~~~~~~~~~~~~~~~~ - -If none of the previous methods work for you, change the ``umask`` so that the -cache and log directories are group-writable or world-writable (depending -if the web server user and the command line user are in the same group or not). -To achieve this, put the following line at the beginning of the ``bin/console``, -``web/app.php`` and ``web/app_dev.php`` files:: - - umask(0002); // This will let the permissions be 0775 - - // or - - umask(0000); // This will let the permissions be 0777 - -.. note:: - - Changing the ``umask`` is not thread-safe, so the ACL methods are recommended - when they are available. - -.. _`enable ACL support`: https://help.ubuntu.com/community/FilePermissionsACLs +In Symfony 3.x, you needed to do some extra work to make sure that your cache directory +was writable. But that is no longer true! In Symfony 4, everything works automatically: + +* In the ``dev`` environment, ``umask()`` is used in ``bin/console`` and ``public/index.php`` + so that any created files are writable by everyone. + +* In the ``prod`` environment (i.e. when ``APP_ENV`` is ``prod`` and ``APP_DEBUG`` + is ``0``), as long as you run ``php bin/console cache:warmup``, no cache files + will need to be written to disk at runtime. The only exception is when using + a filesystem-based cache, such as Doctrine's query result cache or Symfony's + cache with a filesystem provider configured. + +* In all environments, the log directory (``var/log/`` by default) must exist + and be writable by your web server user and terminal user. One way this can + be done is by using ``chmod -R 777 var/log/``. Be aware that your logs are + readable by any user on your production system. diff --git a/setup/flex.rst b/setup/flex.rst index cb969279485..87d40dd6c70 100644 --- a/setup/flex.rst +++ b/setup/flex.rst @@ -1,192 +1,167 @@ .. index:: Flex -Using Symfony Flex to Manage Symfony Applications -================================================= +Upgrading Existing Applications to Symfony Flex +=============================================== -`Symfony Flex`_ is the new way to install and manage Symfony applications. Flex -is not a new Symfony version, but a tool that replaces and improves the -`Symfony Installer`_ and the `Symfony Standard Edition`_. - -Symfony Flex **automates the most common tasks of Symfony applications**, like -installing and removing bundles and other Composer dependencies. Symfony -Flex works for Symfony 3.3 and higher. Starting from Symfony 4.0, Flex -should be used by default, but it is still optional. - -How Does Flex Work ------------------- - -Symfony Flex is a Composer plugin that modifies the behavior of the -``require``, ``update``, and ``remove`` commands. When installing or removing -dependencies in a Flex-enabled application, Symfony can perform tasks before -and after the execution of Composer tasks. - -Consider the following example: - -.. code-block:: terminal - - $ cd my-project/ - $ composer require mailer - -If you execute that command in a Symfony application which doesn't use Flex, -you'll see a Composer error explaining that ``mailer`` is not a valid package -name. However, if the application has Symfony Flex installed, that command ends -up installing and enabling the SwiftmailerBundle, which is the best way to -integrate Swift Mailer, the official mailer for Symfony applications. - -When Symfony Flex is installed in the application and you execute ``composer -require``, the application makes a request to the Symfony Flex server before -trying to install the package with Composer. - -* If there's no information about that package, the Flex server returns nothing and - the package installation follows the usual procedure based on Composer; +Using Symfony Flex is optional, even in Symfony 4, where Flex is used by +default. However, Flex is so convenient and improves your productivity so much +that it's strongly recommended to upgrade your existing applications to it. -* If there's special information about that package, Flex returns it in a file - called a "recipe" and the application uses it to decide which package to - install and which automated tasks to run after the installation. +Symfony Flex recommends that applications use the following directory structure, +which is the same used by default in Symfony 4, but you can +:ref:`customize some directories `: -In the above example, Symfony Flex asks about the ``mailer`` package and the -Symfony Flex server detects that ``mailer`` is in fact an alias for -SwiftmailerBundle and returns the "recipe" for it. +.. code-block:: text -Flex keeps tracks of the recipes it installed in a ``symfony.lock`` file, which -must be committed to your code repository. + your-project/ + ├── assets/ + ├── bin/ + │ └── console + ├── config/ + │ ├── bundles.php + │ ├── packages/ + │ ├── routes.yaml + │ └── services.yaml + ├── public/ + │ └── index.php + ├── src/ + │ ├── ... + │ └── Kernel.php + ├── templates/ + ├── tests/ + ├── translations/ + ├── var/ + └── vendor/ -Symfony Flex Recipes -~~~~~~~~~~~~~~~~~~~~ +This means that installing the ``symfony/flex`` dependency in your application +is not enough. You must also upgrade the directory structure to the one shown +above. There's no automatic tool to make this upgrade, so you must follow these +manual steps: -Recipes are defined in a ``manifest.json`` file and can contain any number of -other files and directories. For example, this is the ``manifest.json`` for -SwiftmailerBundle: +#. Install Flex as a dependency of your project: -.. code-block:: javascript + .. code-block:: terminal - { - "bundles": { - "Symfony\\Bundle\\SwiftmailerBundle\\SwiftmailerBundle": ["all"] - }, - "copy-from-recipe": { - "config/": "%CONFIG_DIR%/" - }, - "env": { - "MAILER_URL": "smtp://localhost:25?encryption=&auth_mode=" - }, - "aliases": ["mailer", "mail"] - } + $ composer require symfony/flex -The ``aliases`` option allows Flex to install packages using short and easy to -remember names (``composer require mailer`` vs -``composer require symfony/swiftmailer-bundle``). The ``bundles`` option tells -Flex in which environments this bundle should be enabled automatically (``all`` -in this case). The ``env`` option makes Flex add new environment variables to -the application. Finally, the ``copy-from-recipe`` option allows the recipe to -copy files and directories into your application. +#. If the project's ``composer.json`` file contains ``symfony/symfony`` dependency, + it still depends on the Symfony Standard Edition, which is no longer available + in Symfony 4. First, remove this dependency: -The instructions defined in this ``manifest.json`` file are also used by -Symfony Flex when uninstalling dependencies (e.g. ``composer remove mailer``) -to undo all changes. This means that Flex can remove the SwiftmailerBundle from -the application, delete the ``MAILER_URL`` environment variable and any other -file and directory created by this recipe. + .. code-block:: terminal -Symfony Flex recipes are contributed by the community and they are stored in -two public repositories: + $ composer remove symfony/symfony -* `Main recipe repository`_, is a curated list of recipes for high quality and - maintained packages. Symfony Flex only looks in this repository by default. + Now add the ``symfony/symfony`` package to the ``conflict`` section of the project's + ``composer.json`` file as `shown in this example of the skeleton-project`_ so that + it will not be installed again: -* `Contrib recipe repository`_, contains all the recipes created by the - community. All of them are guaranteed to work, but their associated packages - could be unmaintained. Symfony Flex will ask your permission before installing - any of these recipes. + .. code-block:: diff -Read the `Symfony Recipes documentation`_ to learn everything about how to -create recipes for your own packages. + { + "require": { + "symfony/flex": "^1.0", + + }, + + "conflict": { + + "symfony/symfony": "*" + } + } -Using Symfony Flex in New Applications --------------------------------------- + Now you must add in ``composer.json`` all the Symfony dependencies required + by your project. A quick way to do that is to add all the components that + were included in the previous ``symfony/symfony`` dependency and later you + can remove anything you don't really need: -Symfony has published a new "skeleton" project, which is a minimal Symfony -project recommended to create new applications. This "skeleton" already -includes Symfony Flex as a dependency. This means you can create a new Flex-enabled -Symfony application by executing the following command: + .. code-block:: terminal -.. code-block:: terminal + $ composer require annotations asset orm-pack twig \ + logger mailer form security translation validator + $ composer require --dev dotenv maker-bundle orm-fixtures profiler - $ composer create-project symfony/skeleton:3.4.* my-project +#. If the project's ``composer.json`` file doesn't contain the ``symfony/symfony`` + dependency, it already defines its dependencies explicitly, as required by + Flex. Reinstall all dependencies to force Flex to generate the + configuration files in ``config/``, which is the most tedious part of the upgrade + process: -.. note:: + .. code-block:: terminal - The use of the Symfony Installer to create new applications is no longer - recommended since Symfony 3.3. Use the Composer ``create-project`` command - instead. + $ rm -rf vendor/* + $ composer install -Upgrading Existing Applications to Flex ---------------------------------------- +#. No matter which of the previous steps you followed. At this point, you'll have + lots of new config files in ``config/``. They contain the default config + defined by Symfony, so you must check your original files in ``app/config/`` + and make the needed changes in the new files. Flex config doesn't use suffixes + in config files, so the old ``app/config/config_dev.yml`` goes to + ``config/packages/dev/*.yaml``, etc. -Using Symfony Flex is optional, even in Symfony 4, where Flex will be used by -default. However, Flex is so convenient and improves your productivity so much -that it's strongly recommended to upgrade your existing applications to it. +#. The most important config file is ``app/config/services.yml``, which now is + located at ``config/services.yaml``. Copy the contents of the + `default services.yaml file`_ and then add your own service configuration. + Later you can revisit this file because thanks to Symfony's + :doc:`autowiring feature ` you can remove + most of the service configuration. -The only caveat is that Symfony Flex requires that applications use the -following directory structure, which is the same used by default in Symfony 4: + .. note:: -.. code-block:: text + Make sure that your previous configuration files don't have ``imports`` + declarations pointing to resources already loaded by ``Kernel::configureContainer()`` + or ``Kernel::configureRoutes()`` methods. - your-project/ - ├── config/ - │ ├── bundles.php - │ ├── packages/ - │ ├── routes.yaml - │ └── services.yaml - ├── public/ - │ └── index.php - ├── src/ - │ ├── ... - │ └── Kernel.php - ├── templates/ - └── vendor/ +#. Move the rest of the ``app/`` contents as follows (and after that, remove the + ``app/`` directory): -This means that installing the ``symfony/flex`` dependency in your application -is not enough. You must also upgrade the directory structure to the one shown -above. There's no automatic tool to make this upgrade, so you must follow these -manual steps: + * ``app/Resources/views/`` -> ``templates/`` + * ``app/Resources/translations/`` -> ``translations/`` + * ``app/Resources//views/`` -> ``templates/bundles//`` + * rest of ``app/Resources/`` files -> ``src/Resources/`` -#. Create a new empty Symfony application (``composer create-project - symfony/skeleton my-project-flex``) +#. Move the original PHP source code from ``src/AppBundle/*``, except bundle + specific files (like ``AppBundle.php`` and ``DependencyInjection/``), to + ``src/``. -#. Merge the ``require`` and ``require-dev`` dependencies defined in your - original project's ``composer.json`` file to the ``composer.json`` file of the - new project (don't copy the ``symfony/symfony`` dependency, but add the - relevant components you are effectively using in your project). + In addition to moving the files, update the ``autoload`` and ``autoload-dev`` + values of the ``composer.json`` file as `shown in this example`_ to use + ``App\`` and ``App\Tests\`` as the application namespaces (advanced IDEs can + do this automatically). -#. Install the dependencies in the new project executing ``composer update``. - This will make Symfony Flex generate all the configuration files in - ``config/packages/`` + If you used multiple bundles to organize your code, you must reorganize your + code into ``src/``. For example, if you had ``src/UserBundle/Controller/DefaultController.php`` + and ``src/ProductBundle/Controller/DefaultController.php``, you could move + them to ``src/Controller/UserController.php`` and ``src/Controller/ProductController.php``. -#. Review the generated ``config/packages/*.yaml`` files and make any needed - changes according to the configuration defined in the - ``app/config/config_*.yml`` file of your original project. Beware that this is - the most time-consuming and error-prone step of the upgrade process. +#. Move the public assets, such as images or compiled CSS/JS files, from + ``src/AppBundle/Resources/public/`` to ``public/`` (e.g. ``public/images/``). -#. Move the original parameters defined in ``app/config/parameters.*.yml`` to - the new ``config/services.yaml`` and ``.env`` files depending on your needs. - If you have defined :doc:`custom config options in your bundles ` - move them to the new ``config/services.yaml`` and ``.env`` files. +#. Move the source of the assets (e.g. the SCSS files) to ``assets/`` and use + :doc:`Webpack Encore ` to manage and compile them. #. ``SYMFONY_DEBUG`` and ``SYMFONY_ENV`` environment variables were replaced by ``APP_DEBUG`` and ``APP_ENV``. Copy their values to the new vars and then remove the former ones. +#. Create the new ``public/index.php`` front controller + `copying Symfony's index.php source`_ and, if you made any customization in + your ``web/app.php`` and ``web/app_dev.php`` files, copy those changes into + the new file. You can now remove the old ``web/`` dir. + +#. Update the ``bin/console`` script `copying Symfony's bin/console source`_ + and changing anything according to your original console script. + +#. Remove ``src/AppBundle/``. + #. Move the original source code from ``src/{App,...}Bundle/`` to ``src/`` and update the namespaces of every PHP file to be ``App\...`` (advanced IDEs can do this automatically). -#. Move the original templates from ``app/Resources/views/`` to ``templates/`` - and ``app/Resources/translations`` to ``translations/``. There may be a few - other files you need to move into a new location. +#. Remove the ``bin/symfony_requirements`` script and if you need a replacement + for it, use the new `Symfony Requirements Checker`_. + +#. Update the ``.gitignore`` file to replace the existing ``var/logs/`` entry + by ``var/log/``, which is the new name for the log directory. -#. Make any other change needed by your application. For example, if your - original ``web/app_*.php`` front controllers were customized, add those changes - to the new ``public/index.php`` controller. +.. _flex-customize-paths: Customizing Flex Paths ---------------------- @@ -218,9 +193,9 @@ If you customize these paths, some files copied from a recipe still may contain references to the original path. In other words: you may need to update some things manually after a recipe is installed. -.. _`Symfony Flex`: https://github.com/symfony/flex -.. _`Symfony Installer`: https://github.com/symfony/symfony-installer -.. _`Symfony Standard Edition`: https://github.com/symfony/symfony-standard -.. _`Main recipe repository`: https://github.com/symfony/recipes -.. _`Contrib recipe repository`: https://github.com/symfony/recipes-contrib -.. _`Symfony Recipes documentation`: https://github.com/symfony/recipes/blob/master/README.rst +.. _`default services.yaml file`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/3.3/config/services.yaml +.. _`shown in this example`: https://github.com/symfony/skeleton/blob/8e33fe617629f283a12bbe0a6578bd6e6af417af/composer.json#L24-L33 +.. _`shown in this example of the skeleton-project`: https://github.com/symfony/skeleton/blob/8e33fe617629f283a12bbe0a6578bd6e6af417af/composer.json#L44-L46 +.. _`copying Symfony's index.php source`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/3.3/public/index.php +.. _`copying Symfony's bin/console source`: https://github.com/symfony/recipes/blob/master/symfony/console/3.3/bin/console +.. _`Symfony Requirements Checker`: https://github.com/symfony/requirements-checker diff --git a/setup/homestead.rst b/setup/homestead.rst index e3a8ebc5542..c68790800d3 100644 --- a/setup/homestead.rst +++ b/setup/homestead.rst @@ -49,10 +49,13 @@ configuration: # ... sites: - map: symfony-demo.test - to: /home/vagrant/projects/symfony_demo/web - type: symfony + to: /home/vagrant/projects/symfony_demo/public + type: symfony4 The ``type`` option tells Homestead to use the Symfony nginx configuration. +Homestead now supports a Symfony 2 and 3 web layout with ``app.php`` and +``app_dev.php`` when using type ``symfony2`` and an ``index.php`` layout when +using type ``symfony4``. At last, edit the hosts file on your local machine to map ``symfony-demo.test`` to ``192.168.10.10`` (which is the IP used by Homestead):: diff --git a/setup/new_project_git.rst b/setup/new_project_git.rst deleted file mode 100644 index 040a9bcd581..00000000000 --- a/setup/new_project_git.rst +++ /dev/null @@ -1,99 +0,0 @@ -.. index:: - single: Set Up; Git - -.. _how-to-create-and-store-a-symfony2-project-in-git: - -How to Create and Store a Symfony Project in Git -================================================ - -.. tip:: - - Though this article is specifically about Git, the same generic principles - will apply if you're storing your project in Subversion. - -Once you've read through :doc:`/page_creation` and become familiar with -using Symfony, you'll no-doubt be ready to start your own project. In this -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 :doc:`/setup` article for details. - -Once your project is running, just follow these simple steps: - -#. Initialize your Git repository: - - .. code-block:: terminal - - $ git init - -#. Add all of the initial files to Git: - - .. code-block:: terminal - - $ git add . - - .. tip:: - - 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. - - .. tip:: - - 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: - - .. code-block:: terminal - - $ 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 :doc:`/page_creation` article -to learn more about how to configure and develop inside your application. - -.. tip:: - - The Symfony Standard Edition comes with some example functionality. To - remove the sample code, follow the instructions in the - ":doc:`/bundles/remove`" article. - -.. include:: _vendor_deps.rst.inc - -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`_. 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`_. - -.. _`Git`: https://git-scm.com/ -.. _`GitHub`: https://github.com/ -.. _`barebones repository`: https://git-scm.com/book/en/Git-Basics-Getting-a-Git-Repository -.. _`Gitolite`: https://github.com/sitaramc/gitolite -.. _`GitHub .gitignore`: https://help.github.com/articles/ignoring-files -.. _`Bitbucket`: https://bitbucket.org/ -.. _`comparison of hosting services`: https://en.wikipedia.org/wiki/Comparison_of_open-source_software_hosting_facilities diff --git a/setup/new_project_svn.rst b/setup/new_project_svn.rst deleted file mode 100644 index 9a16193d0d2..00000000000 --- a/setup/new_project_svn.rst +++ /dev/null @@ -1,142 +0,0 @@ -.. index:: - single: Set Up; Subversion - -.. _how-to-create-and-store-a-symfony2-project-in-subversion: - -How to Create and Store a Symfony Project in Subversion -======================================================= - -.. tip:: - - This article is specifically about Subversion, and based on principles found - in :doc:`/setup/new_project_git`. - -Once you've read through :doc:`/page_creation` 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 article, you'll learn how -to manage your project using `SVN`_ in a similar manner you would do with -`Git`_. - -.. tip:: - - This is **a** method to tracking your Symfony project in a Subversion - repository. There are several ways to do and this one is one that works. - -The Subversion Repository -------------------------- - -For this article it's assumed that your repository layout follows the -widespread standard structure: - -.. code-block:: text - - myproject/ - branches/ - tags/ - trunk/ - -.. tip:: - - 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 :ref:`svn-hosting`). - -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 -:doc:`Installation ` article. - -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``: - - .. code-block:: terminal - - $ svn checkout http://myproject.googlecode.com/svn/trunk myproject - -#. Copy the Symfony project files in the Subversion folder: - - .. code-block:: terminal - - $ 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. - - .. code-block:: terminal - - $ cd myproject/ - $ svn add --depth=empty app var var/cache var/logs app/config web - - $ svn propset svn:ignore "vendor" . - $ svn propset svn:ignore "bootstrap*" var/ - $ svn propset svn:ignore "parameters.yml" app/config/ - $ svn propset svn:ignore "*" var/cache/ - $ svn propset svn:ignore "*" var/logs/ - $ svn propset svn:ignore "*" var/sessions/ - - $ svn propset svn:ignore "bundles" web - - $ svn ci -m "commit basic Symfony ignore list (vendor, var/bootstrap*, app/config/parameters.yml, var/cache/*, var/logs/*, web/bundles)" - -#. The rest of the files can now be added and committed to the project: - - .. code-block:: terminal - - $ svn add --force . - $ svn ci -m "add basic Symfony Standard 3.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 :doc:`/page_creation` article -to learn more about how to configure and develop inside your application. - -.. tip:: - - The Symfony Standard Edition comes with some example functionality. To - remove the sample code, follow the instructions in the - ":doc:`/bundles/remove`" article. - -.. include:: _vendor_deps.rst.inc - -.. _svn-hosting: - -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. - -.. _`Git`: https://git-scm.com/ -.. _`SVN`: https://subversion.apache.org/ -.. _`Subversion`: https://subversion.apache.org/ -.. _`Version Control with Subversion`: http://svnbook.red-bean.com/ -.. _`GitHub`: https://github.com/ -.. _`Google code`: https://code.google.com/hosting/ -.. _`SourceForge`: https://sourceforge.net/ -.. _`Gna`: http://gna.org/ diff --git a/setup/symfony_server.rst b/setup/symfony_server.rst index 2e38b29884d..7d0f31e09f8 100644 --- a/setup/symfony_server.rst +++ b/setup/symfony_server.rst @@ -14,9 +14,8 @@ PHP application and even with HTML/SPA (single page applications). Installation ------------ -The Symfony server is distributed as a free installable binary and has support -for Linux, macOS and Windows. Go to `symfony.com/download`_ and follow the -instructions for your operating system. +The Symfony server is part of the ``symfony`` binary created when you +`install Symfony`_ and has support for Linux, macOS and Windows. .. note:: @@ -332,59 +331,8 @@ debug any issues. `Read SymfonyCloud technical docs`_. -Bonus Features --------------- - -In addition to being a local web server, the Symfony server provides other -useful features: - -Looking for Security Vulnerabilities -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Instead of installing the :doc:`Symfony Security Checker ` -as a dependency of your projects, you can run the following command: - -.. code-block:: terminal - - $ symfony security:check - -This command uses the same vulnerability database as the Symfony Security -Checker but it does not make HTTP calls to the official API endpoint. Everything -(except cloning the public database) is done locally, which is the best for CI -(*continuous integration*) scenarios. - -Creating Symfony Projects -~~~~~~~~~~~~~~~~~~~~~~~~~ - -In addition to the `different ways of installing Symfony`_, you can use these -commands from the Symfony server: - -.. code-block:: terminal - - # creates a new project based on symfony/skeleton - $ symfony new my_project_name --version=3.4 - - # creates a new project based on symfony/website-skeleton - $ symfony new my_project_name --version=3.4 --full - - # creates a new project based on the Symfony Demo application - $ symfony new my_project_name --version=3.4 --demo - -You can create a project depending on a **development** version as well (note -that Composer will also set the stability to ``dev`` for all root dependencies): - -.. code-block:: terminal - - # creates a new project based on Symfony's master branch (both are equivalent) - $ symfony new my_project_name --version=dev-master - $ symfony new my_project_name --version=next - - # creates a new project based on Symfony's 4.3 dev branch - $ symfony new my_project_name --version=4.3.x-dev - -.. _`symfony.com/download`: https://symfony.com/download +.. _`install Symfony`: https://symfony.com/download .. _`symfony/cli`: https://github.com/symfony/cli -.. _`different ways of installing Symfony`: https://symfony.com/download .. _`Docker`: https://en.wikipedia.org/wiki/Docker_(software) .. _`SymfonyCloud`: https://symfony.com/cloud/ .. _`Read SymfonyCloud technical docs`: https://symfony.com/doc/master/cloud/intro.html diff --git a/setup/unstable_versions.rst b/setup/unstable_versions.rst index 0e6c32be47e..be199a783d5 100644 --- a/setup/unstable_versions.rst +++ b/setup/unstable_versions.rst @@ -7,54 +7,50 @@ they are released as stable versions. Creating a New Project Based on an Unstable Symfony Version ----------------------------------------------------------- -Suppose that Symfony 2.7 version hasn't been released yet and you want to create + +Suppose that the Symfony 4.0 version hasn't been released yet and you want to create a new project to test its features. First, `install the Composer package manager`_. Then, open a command console, enter your project's directory and execute the following command: .. code-block:: terminal - $ composer create-project symfony/framework-standard-edition my_project "2.7.*" --stability=dev + # Download the absolute latest commit + $ composer create-project symfony/skeleton my_project -s dev Once the command finishes its execution, you'll have a new Symfony project created -in the ``my_project/`` directory and based on the most recent code found in the -``2.7`` branch. - -If you want to test a beta version, use ``beta`` as the value of the ``stability`` -option: - -.. code-block:: terminal - - $ composer create-project symfony/framework-standard-edition my_project "2.7.*" --stability=beta +in the ``my_project/`` directory. Upgrading your Project to an Unstable Symfony Version ----------------------------------------------------- -Suppose again that Symfony 2.7 hasn't been released yet and you want to upgrade +Suppose again that Symfony 4.0 hasn't been released yet and you want to upgrade an existing application to test that your project works with it. First, open the ``composer.json`` file located in the root directory of your -project. Then, edit the value of the version defined for the ``symfony/symfony`` -dependency as follows: +project. Then, edit the value of all of the ``symfony/*`` libraries to the +new version and change your ``minimum-stability`` to ``beta``: -.. code-block:: json +.. code-block:: diff { "require": { - "symfony/symfony" : "2.7.*@dev" - } + + "symfony/framework-bundle": "^4.0", + + "symfony/finder": "^4.0", + "...": "..." + }, + + "minimum-stability": "beta" } -Finally, open a command console, enter your project directory and execute the -following command to update your project dependencies: +You can also use set ``minimum-stability`` to ``dev``, or omit this line +entirely, and opt into your stability on each package by using constraints +like ``4.0.*@beta``. -.. code-block:: terminal +Finally, from a terminal, update your project's dependencies: - $ composer update symfony/symfony +.. code-block:: terminal -If you prefer to test a Symfony beta version, replace the ``"2.7.*@dev"`` constraint -by ``"2.7.0-beta1"`` to install a specific beta number or ``2.7.*@beta`` to get -the most recent beta version. + $ composer update After upgrading the Symfony version, read the :ref:`Symfony Upgrading Guide ` to learn how you should proceed to update your application's code in case the new diff --git a/setup/upgrade_major.rst b/setup/upgrade_major.rst index ce7c2a975ea..8e5ec0723ac 100644 --- a/setup/upgrade_major.rst +++ b/setup/upgrade_major.rst @@ -1,7 +1,7 @@ .. index:: single: Upgrading; Major Version -Upgrading a Major Version (e.g. 2.7.0 to 3.0.0) +Upgrading a Major Version (e.g. 4.4.0 to 5.0.0) =============================================== Every two years, Symfony releases a new major version release (the first number @@ -30,21 +30,23 @@ backwards incompatible changes. To accomplish this, the "old" (e.g. functions, classes, etc) code still works, but is marked as *deprecated*, indicating that it will be removed/changed in the future and that you should stop using it. -When the major version is released (e.g. 3.0.0), all deprecated features and +When the major version is released (e.g. 5.0.0), all deprecated features and functionality are removed. So, as long as you've updated your code to stop using these deprecated features in the last version before the major (e.g. -2.8.*), you should be able to upgrade without a problem. +``4.4.*``), you should be able to upgrade without a problem. That means that +you should first :doc:`upgrade to the last minor version ` +(e.g. 4.4) so that you can see *all* the deprecations. -To help you with this, deprecation notices are triggered whenever you end up +To help you find deprecations, notices are triggered whenever you end up using a deprecated feature. When visiting your application in the -:doc:`dev environment ` +:ref:`dev environment ` in your browser, these notices are shown in the web dev toolbar: .. image:: /_images/install/deprecations-in-profiler.png :align: center :class: with-browser -Ultimately, you want to stop using the deprecated functionality. +Ultimately, you should aim to stop using the deprecated functionality. Sometimes, this is easy: the warning might tell you exactly what to change. But other times, the warning might be unclear: a setting somewhere might @@ -73,15 +75,16 @@ All you need to do is install the PHPUnit bridge: Now, you can start fixing the notices: -.. code-block:: text +.. code-block:: terminal - $ phpunit + # this command is available after running "composer require --dev symfony/phpunit-bridge" + $ ./bin/phpunit ... OK (10 tests, 20 assertions) Remaining deprecation notices (6) - + The "request" service is deprecated and will be removed in 3.0. Add a type-hint for Symfony\Component\HttpFoundation\Request to your controller parameters to retrieve the request instead: 6x @@ -95,10 +98,10 @@ done! .. sidebar:: Using the Weak Deprecations Mode Sometimes, you can't fix all deprecations (e.g. something was deprecated - in 2.8 and you still need to support 2.7). In these cases, you can still - use the bridge to fix as many deprecations as possible and then switch - to the weak test mode to make your tests pass again. You can do this by - using the ``SYMFONY_DEPRECATIONS_HELPER`` env variable: + in 4.4 and you still need to support 4.3). In these cases, you can still + use the bridge to fix as many deprecations as possible and then allow + more of them to make your tests pass again. You can do this by using the + ``SYMFONY_DEPRECATIONS_HELPER`` env variable: .. code-block:: xml @@ -107,17 +110,15 @@ done! - + - (you can also execute the command like ``SYMFONY_DEPRECATIONS_HELPER=weak phpunit``). + You can also execute the command like: -.. tip:: + .. code-block:: terminal - Some members of the Symfony Community have developed a tool called - `Symfony-Upgrade-Fixer`_ which automatically fixes some of the most common - deprecations found when upgrading from Symfony 2 to Symfony 3. + $ SYMFONY_DEPRECATIONS_HELPER=max[total]=999999 php ./bin/phpunit .. _upgrade-major-symfony-composer: @@ -125,24 +126,63 @@ done! ----------------------------------------------- Once your code is deprecation free, you can update the Symfony library via -Composer by modifying your ``composer.json`` file: +Composer by modifying your ``composer.json`` file and changing all the libraries +starting with ``symfony/`` to the new major version: -.. code-block:: json +.. code-block:: diff { "...": "...", "require": { - "symfony/symfony": "3.0.*", + - "symfony/cache": "4.3.*", + + "symfony/cache": "4.4.*", + - "symfony/config": "4.3.*", + + "symfony/config": "4.4.*", + - "symfony/console": "4.3.*", + + "symfony/console": "4.4.*", + "...": "...", + + "...": "A few libraries starting with + symfony/ follow their versioning scheme. You + do not need to update these versions: you can + upgrade them independently whenever you want", + "symfony/monolog-bundle": "^3.5", }, - "...": "..." + "...": "...", + } + +Your ``composer.json`` file should also have an ``extra`` block that you will +*also* need to update: + +.. code-block:: diff + + "extra": { + "symfony": { + "...": "...", + - "require": "4.4.*" + + "require": "5.0.*" + } + } + +At the bottom of your ``composer.json`` file, in the ``extra`` block you can +find a data setting for the Symfony version. Make sure to also upgrade +this one. For instance, update it to ``5.0.*`` to upgrade to Symfony 5.0: + +.. code-block:: json + + "extra": { + "symfony": { + "allow-contrib": false, + "require": "5.0.*" + } } Next, use Composer to download new versions of the libraries: .. code-block:: terminal - $ composer update symfony/symfony + $ composer update symfony/* .. include:: /setup/_update_dep_errors.rst.inc @@ -153,10 +193,6 @@ Next, use Composer to download new versions of the libraries: 3) Update your Code to Work with the New Version ------------------------------------------------ -There is a good chance that you're done now! However, the next major version -*may* also contain new BC breaks as a BC layer is not always a possibility. -Make sure you read the ``UPGRADE-X.0.md`` (where X is the new major version) -included in the Symfony repository for any BC break that you need to be aware -of. - -.. _`Symfony-Upgrade-Fixer`: https://github.com/umpirsky/Symfony-Upgrade-Fixer +In some rare situations, the next major version *may* contain backwards-compatibility +breaks. Make sure you read the ``UPGRADE-X.0.md`` (where X is the new major version) +included in the Symfony repository for any BC break that you need to be aware of. diff --git a/setup/upgrade_minor.rst b/setup/upgrade_minor.rst index 7c9f185a0a0..4add91c12da 100644 --- a/setup/upgrade_minor.rst +++ b/setup/upgrade_minor.rst @@ -1,7 +1,7 @@ .. index:: single: Upgrading; Minor Version -Upgrading a Minor Version (e.g. 2.5.3 to 2.6.1) +Upgrading a Minor Version (e.g. 4.0.0 to 4.1.0) =============================================== If you're upgrading a minor version (where the middle number changes), then @@ -21,25 +21,52 @@ There are two steps to upgrading a minor 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: +The ``composer.json`` file is configured to allow Symfony packages to be +upgraded to patch versions. But to upgrade to a new minor version, you will +probably need to update the version constraint next to each library starting +``symfony/``. Suppose you are upgrading from Symfony 4.3 to 4.4: -.. code-block:: json +.. code-block:: diff { "...": "...", "require": { - "symfony/symfony": "2.6.*", + - "symfony/cache": "4.3.*", + + "symfony/cache": "4.4.*", + - "symfony/config": "4.3.*", + + "symfony/config": "4.4.*", + - "symfony/console": "4.3.*", + + "symfony/console": "4.4.*", + "...": "...", + + "...": "A few libraries starting with + symfony/ follow their versioning scheme. You + do not need to update these versions: you can + upgrade them independently whenever you want", + "symfony/monolog-bundle": "^3.5", }, "...": "...", } +Your ``composer.json`` file should also have an ``extra`` block that you will +*also* need to update: + +.. code-block:: diff + + "extra": { + "symfony": { + "...": "...", + - "require": "4.3.*" + + "require": "4.4.*" + } + } + Next, use Composer to download new versions of the libraries: .. code-block:: terminal - $ composer update symfony/symfony + $ composer update "symfony/*" .. include:: /setup/_update_dep_errors.rst.inc @@ -55,12 +82,52 @@ to your code to get everything working. Additionally, some features you're using might still work, but might now be deprecated. While that's just fine, if you know about these deprecations, you can start to fix them over time. -Every version of Symfony comes with an UPGRADE file (e.g. `UPGRADE-2.7.md`_) +Every version of Symfony comes with an UPGRADE file (e.g. `UPGRADE-4.4.md`_) included in the Symfony directory that describes these changes. If you follow the instructions in the document and update your code accordingly, it should be safe to update in the future. These documents can also be found in the `Symfony Repository`_. +.. _updating-flex-recipes: + +3) Updating Recipes +------------------- + +Over time - and especially when you upgrade to a new version of a library - an +updated version of the :ref:`recipe ` may be available. +These updates are usually minor - e.g. new comments in a configuration file - but +it's a good idea to update the core Symfony recipes. + +Symfony Flex provides several commands to help upgrade your recipes. Be sure to +commit any unrelated changes you're working on before starting: + +.. versionadded:: 1.6 + + The recipes commands were introduced in Symfony Flex 1.6. + +.. code-block:: terminal + + # see a list of all installed recipes and which have updates available + $ composer recipes + + # see detailed information about a specific recipes + $ composer recipes symfony/framework-bundle + + # update a specific recipes + $ composer recipes:install symfony/framework-bundle --force -v + +The tricky part of this process is that the recipe "update" does not perform +any intelligent "upgrading" of your code. Instead, **the updates process re-installs +the latest version of the recipe** which means that **your custom code will be +overridden completely**. After updating a recipe, you need to carefully choose +which changes you want, and undo the rest. + +.. admonition:: Screencast + :class: screencast + + For a detailed example, see the `SymfonyCasts Symfony 5 Upgrade Tutorial`_. + .. _`Symfony Repository`: https://github.com/symfony/symfony -.. _`UPGRADE-2.7.md`: https://github.com/symfony/symfony/blob/2.7/UPGRADE-2.7.md +.. _`UPGRADE-4.4.md`: https://github.com/symfony/symfony/blob/4.4/UPGRADE-4.4.md +.. _`SymfonyCasts Symfony 5 Upgrade Tutorial`: https://symfonycasts.com/screencast/symfony5-upgrade diff --git a/setup/upgrade_patch.rst b/setup/upgrade_patch.rst index 5e2c76e55cd..632f6602550 100644 --- a/setup/upgrade_patch.rst +++ b/setup/upgrade_patch.rst @@ -1,22 +1,17 @@ .. index:: single: Upgrading; Patch Version -Upgrading a Patch Version (e.g. 2.6.0 to 2.6.1) +Upgrading a Patch Version (e.g. 5.0.0 to 5.0.1) =============================================== When a new patch version is released (only the last number changed), it is a release that only contains bug fixes. This means that upgrading to a new patch -version is *really* easy: +version should not cause any problems. -.. code-block:: terminal - - $ 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. +To upgrade to a new "patch" release, read the +:doc:`Upgrading a Minor Version ` article. Thanks to +Symfony's :doc:`backwards compatibility promise `, it's +always safe to upgrade to the latest "minor" version. .. tip:: diff --git a/setup/web_server_configuration.rst b/setup/web_server_configuration.rst index 5fc2eea3c32..bda1c343a9d 100644 --- a/setup/web_server_configuration.rst +++ b/setup/web_server_configuration.rst @@ -9,31 +9,50 @@ The preferred way to develop your Symfony application is to use However, 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 Apache or nginx. +Symfony with Apache or Nginx. When using Apache, you can configure PHP as an :ref:`Apache module ` or with FastCGI using :ref:`PHP FPM `. FastCGI also is the preferred way -to use PHP :ref:`with nginx `. +to use PHP :ref:`with Nginx `. -.. sidebar:: The Web Directory +.. sidebar:: The public directory - The web directory is the home of all of your application's public and + The public directory is the home of all of your application's public and static files, including images, stylesheets and JavaScript files. It is - also where the front controllers (``app.php`` and ``app_dev.php``) live. + also where the front controller (``index.php``) lives. - The web directory serves as the document root when configuring your - web server. In the examples below, the ``web/`` directory will be the - document root. This directory is ``/var/www/project/web/``. + The public directory serves as the document root when configuring your + web server. In the examples below, the ``public/`` directory will be the + document root. This directory is ``/var/www/project/public/``. - If your hosting provider requires you to change the ``web/`` directory to + If your hosting provider requires you to change the ``public/`` directory to another location (e.g. ``public_html/``) make sure you - :ref:`override the location of the web/ directory `. + :ref:`override the location of the public/ directory `. .. _web-server-apache-mod-php: -Apache with ``mod_php``/PHP-CGI -------------------------------- +Adding Rewrite Rules +-------------------- + +The easiest way is to install the ``apache`` :ref:`Symfony pack ` +by executing the following command: + +.. code-block:: terminal + + $ composer require symfony/apache-pack + +This pack installs a ``.htaccess`` file in the ``public/`` directory that contains +the rewrite rules. + +.. tip:: + + A performance improvement can be achieved by moving the rewrite rules from the ``.htaccess`` + file into the VirtualHost block of your Apache configuration and then changing + ``AllowOverride All`` to ``AllowOverride None`` in your VirtualHost block. + +Apache with mod_php/PHP-CGI +--------------------------- The **minimum configuration** to get your application running under Apache is: @@ -43,8 +62,8 @@ The **minimum configuration** to get your application running under Apache is: ServerName domain.tld ServerAlias www.domain.tld - DocumentRoot /var/www/project/web - + DocumentRoot /var/www/project/public + AllowOverride All Order Allow,Deny Allow from All @@ -74,15 +93,15 @@ and increase web server performance: ServerName domain.tld ServerAlias www.domain.tld - DocumentRoot /var/www/project/web - DirectoryIndex /app.php + DocumentRoot /var/www/project/public + DirectoryIndex /index.php - + AllowOverride None Order Allow,Deny Allow from All - FallbackResource /app.php + FallbackResource /index.php # uncomment the following lines if you install assets as symlinks @@ -94,16 +113,26 @@ and increase web server performance: # optionally disable the fallback resource for the asset directories # which will allow Apache to return a 404 error when files are # not found instead of passing the request to Symfony - + FallbackResource disabled ErrorLog /var/log/apache2/project_error.log CustomLog /var/log/apache2/project_access.log combined + + # optionally set the value of the environment variables used in the application + #SetEnv APP_ENV prod + #SetEnv APP_SECRET + #SetEnv DATABASE_URL "mysql://db_user:db_pass@host:3306/db_name" +.. caution:: + + Use ``FallbackResource`` on Apache 2.4.25 or higher, due to a bug which was + fixed on that release causing the root ``/`` to hang. + .. tip:: - If you are using ``php-cgi``, Apache does not pass HTTP basic username and + 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: @@ -111,15 +140,15 @@ and increase web server performance: RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] -Using ``mod_php``/PHP-CGI with Apache 2.4 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Using mod_php/PHP-CGI with Apache 2.4 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In Apache 2.4, ``Order Allow,Deny`` has been replaced by ``Require all granted``. Hence, you need to modify your ``Directory`` permission settings as follows: .. code-block:: apache - + Require all granted # ... @@ -154,8 +183,8 @@ listen on. Each pool can also be run under a different UID and GID: ; or listen on a TCP socket listen = 127.0.0.1:9000 -Using ``mod_proxy_fcgi`` with Apache 2.4 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Using mod_proxy_fcgi with Apache 2.4 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you are running Apache 2.4, you can use ``mod_proxy_fcgi`` to pass incoming requests to PHP-FPM. Configure PHP-FPM to listen on a TCP or Unix socket, enable @@ -183,14 +212,14 @@ requests to PHP-FPM. Configure PHP-FPM to listen on a TCP or Unix socket, enable # 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 + # ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9000/var/www/project/public/$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 + # ProxyPassMatch ^/path-to-app/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9000/var/www/project/public/$1 - DocumentRoot /var/www/project/web - + DocumentRoot /var/www/project/public + # enable the .htaccess rewrites AllowOverride All Require all granted @@ -224,8 +253,8 @@ should look something like this: Alias /php7-fcgi /usr/lib/cgi-bin/php7-fcgi FastCgiExternalServer /usr/lib/cgi-bin/php7-fcgi -host 127.0.0.1:9000 -pass-header Authorization - DocumentRoot /var/www/project/web - + DocumentRoot /var/www/project/public + # enable the .htaccess rewrites AllowOverride All Order Allow,Deny @@ -251,43 +280,39 @@ instead: .. _web-server-nginx: -nginx +Nginx ----- -The **minimum configuration** to get your application running under nginx is: +The **minimum configuration** to get your application running under Nginx is: .. code-block:: nginx server { server_name domain.tld www.domain.tld; - root /var/www/project/web; + root /var/www/project/public; 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/php/php7.1-fpm.sock; - fastcgi_split_path_info ^(.+\.php)(/.*)$; - include fastcgi_params; - # When you are using symlinks to link the document root to the - # current version of your application, you should pass the real - # application path instead of the path to the symlink to PHP - # FPM. - # Otherwise, PHP's OPcache may not properly detect changes to - # your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126 - # for more information). - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - fastcgi_param DOCUMENT_ROOT $realpath_root; + # try to serve file directly, fallback to index.php + try_files $uri /index.php$is_args$args; } - # PROD - location ~ ^/app\.php(/|$) { - fastcgi_pass unix:/var/run/php/php7.1-fpm.sock; + + # optionally disable falling back to PHP script for the asset directories; + # nginx will return a 404 error when files are not found instead of passing the + # request to Symfony (improves performance but Symfony's 404 page is not displayed) + # location /bundles { + # try_files $uri =404; + # } + + location ~ ^/index\.php(/|$) { + fastcgi_pass unix:/var/run/php/php7.2-fpm.sock; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; + + # optionally set the value of the environment variables used in the application + # fastcgi_param APP_ENV prod; + # fastcgi_param APP_SECRET ; + # fastcgi_param DATABASE_URL "mysql://db_user:db_pass@host:3306/db_name"; + # When you are using symlinks to link the document root to the # current version of your application, you should pass the real # application path instead of the path to the symlink to PHP @@ -298,7 +323,7 @@ The **minimum configuration** to get your application running under nginx is: fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; # Prevents URIs that include the front controller. This will 404: - # http://domain.tld/app.php/some-path + # http://domain.tld/index.php/some-path # Remove the internal directive to allow URIs like this internal; } @@ -320,27 +345,26 @@ The **minimum configuration** to get your application running under nginx is: .. tip:: - This executes **only** ``app.php``, ``app_dev.php`` and ``config.php`` in - the web directory. All other files ending in ``.php`` will be denied. + This executes **only** ``index.php`` in the public directory. All other files + ending in ".php" will be denied. - If you have other PHP files in your web directory that need to be executed, + If you have other PHP files in your public directory that need to be executed, be sure to include them in the ``location`` block above. .. caution:: - After you deploy to production, make sure that you **cannot** access the ``app_dev.php`` - or ``config.php`` scripts (i.e. ``http://example.com/app_dev.php`` and ``http://example.com/config.php``). - If you *can* access these, be sure to remove the ``DEV`` section from the above configuration. + After you deploy to production, make sure that you **cannot** access the ``index.php`` + script (i.e. ``http://example.com/index.php``). .. note:: By default, Symfony applications include several ``.htaccess`` files to configure redirections and to prevent unauthorized access to some sensitive directories. Those files are only useful when using Apache, so you can - safely remove them when using nginx. + safely remove them when using Nginx. -For advanced nginx configuration options, read the official `nginx documentation`_. +For advanced Nginx configuration options, read the official `Nginx documentation`_. .. _`Apache documentation`: https://httpd.apache.org/docs/ .. _`FastCgiExternalServer`: https://docs.oracle.com/cd/B31017_01/web.1013/q20204/mod_fastcgi.html#FastCgiExternalServer -.. _`nginx documentation`: https://www.nginx.com/resources/wiki/start/topics/recipes/symfony/ +.. _`Nginx documentation`: https://www.nginx.com/resources/wiki/start/topics/recipes/symfony/ diff --git a/templates.rst b/templates.rst new file mode 100644 index 00000000000..2b289af7d93 --- /dev/null +++ b/templates.rst @@ -0,0 +1,1112 @@ +.. index:: + single: Templating + +Creating and Using Templates +============================ + +A template is the best way to organize and render HTML from inside your application, +whether you need to render HTML from a :doc:`controller ` or generate +the :doc:`contents of an email `. Templates in Symfony are created with +Twig: a flexible, fast, and secure template engine. + +.. _twig-language: + +Twig Templating Language +------------------------ + +The `Twig`_ templating language allows you to write concise, readable templates +that are more friendly to web designers and, in several ways, more powerful than +PHP templates. Take a look at the following Twig template example. Even if it's +the first time you see Twig, you probably understand most of it: + +.. code-block:: html+twig + + + + + Welcome to Symfony! + + +

{{ page_title }}

+ + {% if user.isLoggedIn %} + Hello {{ user.name }}! + {% endif %} + + {# ... #} + + + +Twig syntax is based on these three constructs: + +* ``{{ ... }}``, used to display the content of a variable or the result of + evaluating an expression; +* ``{% ... %}``, used to run some logic, such as a conditional or a loop; +* ``{# ... #}``, used to add comments to the template (unlike HTML comments, + these comments are not included in the rendered page). + +You can't run PHP code inside Twig templates, but Twig provides utilities to +run some logic in the templates. For example, **filters** modify content before +being rendered, like the ``upper`` filter to uppercase contents: + +.. code-block:: twig + + {{ title|upper }} + +Twig comes with a long list of `tags`_, `filters`_ and `functions`_ that are +available by default. In Symfony applications you can also use these +:doc:`Twig filters and functions defined by Symfony ` +and you can :doc:`create your own Twig filters and functions `. + +Twig is fast in the ``prod`` :ref:`environment ` +(because templates are compiled into PHP and cached automatically), but +convenient to use in the ``dev`` environment (because templates are recompiled +automatically when you change them). + +Twig Configuration +~~~~~~~~~~~~~~~~~~ + +Twig has several configuration options to define things like the format used +to display numbers and dates, the template caching, etc. Read the +:doc:`Twig configuration reference ` to learn about them. + +Creating Templates +------------------ + +Before explaining in detail how to create and render templates, look at the +following example for a quick overview of the whole process. First, you need to +create a new file in the ``templates/`` directory to store the template contents: + +.. code-block:: html+twig + + {# templates/user/notifications.html.twig #} +

Hello {{ user_first_name }}!

+

You have {{ notifications|length }} new notifications.

+ +Then, create a :doc:`controller ` that renders this template and +passes to it the needed variables:: + + // src/Controller/UserController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + + class UserController extends AbstractController + { + // ... + + public function notifications() + { + // get the user information and notifications somehow + $userFirstName = '...'; + $userNotifications = ['...', '...']; + + // the template path is the relative file path from `templates/` + return $this->render('user/notifications.html.twig', [ + // this array defines the variables passed to the template, + // where the key is the variable name and the value is the variable value + // (Twig recommends using snake_case variable names: 'foo_bar' instead of 'fooBar') + 'user_first_name' => $userFirstName, + 'notifications' => $userNotifications, + ]); + } + } + +Template Naming +~~~~~~~~~~~~~~~ + +Symfony recommends the following for template names: + +* Use `snake case`_ for filenames and directories (e.g. ``blog_posts.twig``, + ``admin/default_theme/blog/index.twig``, etc.); +* Define two extensions for filenames (e.g. ``index.html.twig`` or + ``blog_posts.xml.twig``) being the first extension (``html``, ``xml``, etc.) + the final format that the template will generate. + +Although templates usually generate HTML contents, they can generate any +text-based format. That's why the two-extension convention simplifies the way +templates are created and rendered for multiple formats. + +Template Location +~~~~~~~~~~~~~~~~~ + +Templates are stored by default in the ``templates/`` directory. When a service +or controller renders the ``product/index.html.twig`` template, they are actually +referring to the ``/templates/product/index.html.twig`` file. + +The default templates directory is configurable with the +:ref:`twig.default_path ` option and you can add more +template directories :ref:`as explained later ` in this article. + +Template Variables +~~~~~~~~~~~~~~~~~~ + +A common need for templates is to print the values stored in the templates +passed from the controller or service. Variables usually store objects and +arrays instead of strings, numbers and boolean values. That's why Twig provides +quick access to complex PHP variables. Consider the following template: + +.. code-block:: html+twig + +

{{ user.name }} added this comment on {{ comment.publishedAt|date }}

+ +The ``user.name`` notation means that you want to display some information +(``name``) stored in a variable (``user``). Is ``user`` an array or an object? +Is ``name`` a property or a method? In Twig this doesn't matter. + +When using the ``foo.bar`` notation, Twig tries to get the value of the variable +in the following order: + +#. ``$foo['bar']`` (array and element); +#. ``$foo->bar`` (object and public property); +#. ``$foo->bar()`` (object and public method); +#. ``$foo->getBar()`` (object and *getter* method); +#. ``$foo->isBar()`` (object and *isser* method); +#. ``$foo->hasBar()`` (object and *hasser* method); +#. If none of the above exists, use ``null``. + +This allows to evolve your application code without having to change the +template code (you can start with array variables for the application proof of +concept, then move to objects with methods, etc.) + +.. _templates-link-to-pages: + +Linking to Pages +~~~~~~~~~~~~~~~~ + +Instead of writing the link URLs by hand, use the ``path()`` function to +generate URLs based on the :ref:`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. + +Consider the following routing configuration: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Controller/BlogController.php + namespace App\Controller; + + // ... + use Symfony\Component\Routing\Annotation\Route; + + class BlogController extends AbstractController + { + /** + * @Route("/", name="blog_index") + */ + public function index() + { + // ... + } + + /** + * @Route("/article/{slug}", name="blog_post") + */ + public function show(string $slug) + { + // ... + } + } + + .. code-block:: yaml + + # config/routes.yaml + blog_index: + path: / + controller: App\Controller\BlogController::index + + blog_post: + path: /article/{slug} + controller: App\Controller\BlogController::show + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/routes.php + use App\Controller\BlogController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + $routes->add('blog_index', '/') + ->controller([BlogController::class, 'index']) + ; + + $routes->add('blog_post', '/articles/{slug}') + ->controller([BlogController::class, 'show']) + ; + }; + +Use the ``path()`` Twig function to link to these pages and pass the route name +as the first argument and the route parameters as the optional second argument: + +.. code-block:: html+twig + + Homepage + + {# ... #} + + {% for post in blog_posts %} +

+ {{ post.title }} +

+ +

{{ post.excerpt }}

+ {% endfor %} + +The ``path()`` function generates relative URLs. If you need to generate +absolute URLs (for example when rendering templates for emails or RSS feeds), +use the ``url()`` function, which takes the same arguments as ``path()`` +(e.g. `` ... ``). + +.. _templates-link-to-assets: + +Linking to CSS, JavaScript and Image Assets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a template needs to link to a static asset (e.g. an image), Symfony provides +an ``asset()`` Twig function to help generate that URL. First, install the +``asset`` package: + +.. code-block:: terminal + + $ composer require symfony/asset + +You can now use the ``asset()`` function: + +.. code-block:: html+twig + + {# the image lives at "public/images/logo.png" #} + Symfony! + + {# the CSS file lives at "public/css/blog.css" #} + + + {# the JS file lives at "public/bundles/acme/js/loader.js" #} + + +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. ``https://example.com``), +then the rendered path should be ``/images/logo.png``. But if your application +lives in a subdirectory (e.g. ``https://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. + +.. tip:: + + The ``asset()`` function supports various cache busting techniques via the + :ref:`version `, + :ref:`version_format `, and + :ref:`json_manifest_path ` configuration options. + +.. tip:: + + If you'd like help packaging, versioning and minifying your JavaScript and + CSS assets in a modern way, read about :doc:`Symfony's Webpack Encore `. + +If you need absolute URLs for assets, use the ``absolute_url()`` Twig function +as follows: + +.. code-block:: html+twig + + Symfony! + + + +.. _twig-app-variable: + +The App Global Variable +~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony creates a context object that is injected into every Twig template +automatically as a variable called ``app``. It provides access to some +application information: + +.. code-block:: html+twig + +

Username: {{ app.user.username ?? 'Anonymous user' }}

+ {% if app.debug %} +

Request method: {{ app.request.method }}

+

Application Environment: {{ app.environment }}

+ {% endif %} + +The ``app`` variable (which is an instance of :class:`Symfony\\Bridge\\Twig\\AppVariable`) +gives you access to these variables: + +``app.user`` + The :ref:`current user object ` or ``null`` if the user + is not authenticated. +``app.request`` + The :class:`Symfony\\Component\\HttpFoundation\\Request` object that stores + the current :ref:`request data ` (depending on your + application, this can be a :ref:`sub-request ` + or a regular request). +``app.session`` + The :class:`Symfony\\Component\\HttpFoundation\\Session\\Session` object that + represents the current :doc:`user's session ` or ``null`` if there is none. +``app.flashes`` + An array of all the :ref:`flash messages ` stored in the session. + You can also get only the messages of some type (e.g. ``app.flashes('notice')``). +``app.environment`` + The name of the current :ref:`configuration environment ` + (``dev``, ``prod``, etc). +``app.debug`` + True if in :ref:`debug mode `. False otherwise. +``app.token`` + A :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface` + object representing the security token. + +In addition to the global ``app`` variable injected by Symfony, you can also +:doc:`inject variables automatically to all Twig templates `. + +.. _templates-rendering: + +Rendering Templates +------------------- + +Rendering a Template in Controllers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your controller extends from the :ref:`AbstractController `, +use the ``render()`` helper:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + + class ProductController extends AbstractController + { + public function index() + { + // ... + + return $this->render('product/index.html.twig', [ + 'category' => '...', + 'promotions' => ['...', '...'], + ]); + } + } + +If your controller does not extend from ``AbstractController``, you'll need to +:ref:`fetch services in your controller ` and +use the ``render()`` method of the ``twig`` service. + +Rendering a Template in Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Inject the ``twig`` Symfony service into your own services and use its +``render()`` method. When using :doc:`service autowiring ` +you only need to add an argument in the service constructor and type-hint it with +the :class:`Twig\\Environment` class:: + + // src/Service/SomeService.php + namespace App\Service; + + use Twig\Environment; + + class SomeService + { + private $twig; + + public function __construct(Environment $twig) + { + $this->twig = $twig; + } + + public function someMethod() + { + // ... + + $htmlContents = $this->twig->render('product/index.html.twig', [ + 'category' => '...', + 'promotions' => ['...', '...'], + ]); + } + } + +Rendering a Template in Emails +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Read the docs about the :ref:`mailer and Twig integration `. + +.. _templates-render-from-route: + +Rendering a Template Directly from a Route +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Although templates are usually rendered in controllers and services, you can +render static pages that don't need any variables directly from the route +definition. Use the special ``TemplateController`` provided by Symfony: + +.. configuration-block:: + + .. code-block:: yaml + + # config/routes.yaml + acme_privacy: + path: /privacy + controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController + defaults: + # the path of the template to render + template: 'static/privacy.html.twig' + + # special options defined by Symfony to set the page cache + maxAge: 86400 + sharedAge: 86400 + + # optionally you can define some arguments passed to the template + context: + site_name: 'ACME' + theme: 'dark' + + .. code-block:: xml + + + + + + + + static/privacy.html.twig + + + 86400 + 86400 + + + + ACME + dark + + + + + .. code-block:: php + + // config/routes.php + use Symfony\Bundle\FrameworkBundle\Controller\TemplateController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + $routes->add('acme_privacy', '/privacy') + ->controller(TemplateController::class) + ->defaults([ + // the path of the template to render + 'template' => 'static/privacy.html.twig', + + // special options defined by Symfony to set the page cache + 'maxAge' => 86400, + 'sharedAge' => 86400, + + // optionally you can define some arguments passed to the template + 'context' => [ + 'site_name' => 'ACME', + 'theme' => 'dark', + ] + ]) + ; + }; + +.. versionadded:: 5.1 + + The ``context`` option was introduced in Symfony 5.1. + +Checking if a Template Exists +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Templates are loaded in the application using a `Twig template loader`_, which +also provides a method to check for template existence. First, get the loader:: + + // in a controller extending from AbstractController + $loader = $this->get('twig')->getLoader(); + + // in a service using autowiring + use Twig\Environment; + + public function __construct(Environment $twig) + { + $loader = $twig->getLoader(); + } + +Then, pass the path of the Twig template to the ``exists()`` method of the loader:: + + if ($loader->exists('theme/layout_responsive.html.twig')) { + // the template exists, do something + // ... + } + +Debugging Templates +------------------- + +Symfony provides several utilities to help you debug issues in your templates. + +Linting Twig Templates +~~~~~~~~~~~~~~~~~~~~~~ + +The ``lint:twig`` command checks that your Twig templates don't have any syntax +errors. It's useful to run it before deploying your application to production +(e.g. in your continuous integration server): + +.. code-block:: terminal + + # check all the application templates + $ php bin/console lint:twig + + # you can also check directories and individual templates + $ php bin/console lint:twig templates/email/ + $ php bin/console lint:twig templates/article/recent_list.html.twig + + # you can also show the deprecated features used in your templates + $ php bin/console lint:twig --show-deprecations templates/email/ + +Inspecting Twig Information +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``debug:twig`` command lists all the information available about Twig +(functions, filters, global variables, etc.). It's useful to check if your +:doc:`custom Twig extensions ` are working properly +and also to check the Twig features added when :ref:`installing packages `: + +.. code-block:: terminal + + # list general information + $ php bin/console debug:twig + + # filter output by any keyword + $ php bin/console debug:twig --filter=date + + # pass a template path to show the physical file which will be loaded + $ php bin/console debug:twig @Twig/Exception/error.html.twig + +The Dump Twig Utilities +~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides a :ref:`dump() function ` as an +improved alternative to PHP's ``var_dump()`` function. This function is useful +to inspect the contents of any variable and you can use it in Twig templates too. + +First, make sure that the VarDumper component is installed in the application: + +.. code-block:: terminal + + $ composer require symfony/var-dumper + +Then, use either the ``{% dump %}`` tag or the ``{{ dump() }}`` function +depending on your needs: + +.. code-block:: html+twig + + {# templates/article/recent_list.html.twig #} + {# the contents of this variable are sent to the Web Debug Toolbar + instead of dumping them inside the page contents #} + {% dump articles %} + + {% for article in articles %} + {# the contents of this variable are dumped inside the page contents + and they are visible on the web page #} + {{ dump(article) }} + + + {{ article.title }} + + {% endfor %} + +To avoid leaking sensitive information, the ``dump()`` function/tag is only +available in the ``dev`` and ``test`` :ref:`configuration environments `. +If you try to use it in the ``prod`` environment, you will see a PHP error. + +.. _templates-reuse-contents: + +Reusing Template Contents +------------------------- + +.. _templates-include: + +Including Templates +~~~~~~~~~~~~~~~~~~~ + +If certain Twig code is repeated in several templates, you can extract it into a +single "template fragment" and include it in other templates. Imagine that the +following code to display the user information is repeated in several places: + +.. code-block:: html+twig + + {# templates/blog/index.html.twig #} + + {# ... #} + + +First, create a new Twig template called ``blog/_user_profile.html.twig`` (the +``_`` prefix is optional, but it's a convention used to better differentiate +between full templates and template fragments). + +Then, remove that content from the original ``blog/index.html.twig`` template +and add the following to include the template fragment: + +.. code-block:: twig + + {# templates/blog/index.html.twig #} + + {# ... #} + {{ include('blog/_user_profile.html.twig') }} + +The ``include()`` Twig function takes as argument the path of the template to +include. The included template has access to all the variables of the template +that includes it (use the `with_context`_ option to control this). + +You can also pass variables to the included template. This is useful for example +to rename variables. Imagine that your template stores the user information in a +variable called ``blog_post.author`` instead of the ``user`` variable that the +template fragment expects. Use the following to *rename* the variable: + +.. code-block:: twig + + {# templates/blog/index.html.twig #} + + {# ... #} + {{ include('blog/_user_profile.html.twig', {user: blog_post.author}) }} + +.. _templates-embed-controllers: + +Embedding Controllers +~~~~~~~~~~~~~~~~~~~~~ + +:ref:`Including template fragments ` is useful to reuse the +same content on several pages. However, this technique is not the best solution +in some cases. + +Imagine that the template fragment displays the three most recent blog articles. +To do that, it needs to make a database query to get those articles. When using +the ``include()`` function, you'd need to do the same database query in every +page that includes the fragment. This is not very convenient. + +A better alternative is to **embed the result of executing some controller** +with the ``render()`` and ``controller()`` Twig functions. + +First, create the controller that renders a certain number of recent articles:: + + // src/Controller/BlogController.php + namespace App\Controller; + + // ... + + class BlogController extends AbstractController + { + public function recentArticles($max = 3) + { + // get the recent articles somehow (e.g. making a database query) + $articles = ['...', '...', '...']; + + return $this->render('blog/_recent_articles.html.twig', [ + 'articles' => $articles + ]); + } + } + +Then, create the ``blog/_recent_articles.html.twig`` template fragment (the +``_`` prefix in the template name is optional, but it's a convention used to +better differentiate between full templates and template fragments): + +.. code-block:: html+twig + + {# templates/blog/_recent_articles.html.twig #} + {% for article in articles %} + + {{ article.title }} + + {% endfor %} + +Now you can call to this controller from any template to embed its result: + +.. code-block:: html+twig + + {# templates/base.html.twig #} + + {# ... #} + + +.. _fragments-path-config: + +When using the ``controller()`` function, controllers are not accessed using a +regular Symfony route but through a special URL used exclusively to serve those +template fragments. Configure that special URL in the ``fragments`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + fragments: { path: /_fragment } + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + $container->loadFromExtension('framework', [ + // ... + 'fragments' => ['path' => '/_fragment'], + ]); + +.. caution:: + + Embedding controllers require making requests to those controllers and + rendering some templates as result. This can have a significant impact in + the application performance if you embed lots of controllers. If possible, + :doc:`cache the template fragment `. + +.. seealso:: + + Templates can also :doc:`embed contents asynchronously ` + with the ``hinclude.js`` JavaScript library. + +Template Inheritance and Layouts +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As your application grows you'll find more and more repeated elements between +pages, such as headers, footers, sidebars, etc. :ref:`Including templates ` +and :ref:`embedding controllers ` can help, but +when pages share a common structure, it's better to use **inheritance**. + +The concept of `Twig template inheritance`_ is similar to PHP class inheritance. +You define a parent template that other templates can extend from and child +templates can override parts of the parent template. + +Symfony recommends the following three-level template inheritance for medium and +complex applications: + +* ``templates/base.html.twig``, defines the common elements of all application + templates, such as ````, ``
``, ``