diff --git a/.doctor-rst.yaml b/.doctor-rst.yaml index 6ef4959d8f9..7323d53d1dd 100644 --- a/.doctor-rst.yaml +++ b/.doctor-rst.yaml @@ -11,7 +11,7 @@ rules: ensure_link_definition_contains_valid_url: ~ ensure_order_of_code_blocks_in_configuration_block: ~ extend_abstract_controller: ~ - extension_xlf_instead_of_xliff: ~ + # extension_xlf_instead_of_xliff: ~ indention: ~ lowercase_as_in_use_statements: ~ max_blank_lines: @@ -51,18 +51,18 @@ rules: yarn_dev_option_at_the_end: ~ # no_app_bundle: ~ - # 4.x + # master versionadded_directive_major_version: - major_version: 4 + major_version: 6 versionadded_directive_min_version: - min_version: '4.0' + min_version: '6.0' deprecated_directive_major_version: - major_version: 4 + major_version: 6 deprecated_directive_min_version: - min_version: '4.0' + min_version: '6.0' # do not report as violation whitelist: @@ -71,6 +71,7 @@ whitelist: - '/``.yml``/' - '/(.*)\.orm\.yml/' # currently DoctrineBundle only supports .yml - '/rst-class/' + - /docker-compose\.yml/ lines: - 'in config files, so the old ``app/config/config_dev.yml`` goes to' - '#. The most important config file is ``app/config/services.yml``, which now is' @@ -84,17 +85,23 @@ whitelist: - '.. versionadded:: 2.4.0' # SwiftMailer - '.. versionadded:: 1.30' # Twig - '.. versionadded:: 1.35' # Twig - - '.. versionadded:: 1.2' # MakerBundle - - '.. versionadded:: 1.11' # MakerBundle - - '.. versionadded:: 1.3' # MakerBundle - - '.. versionadded:: 1.8' # MakerBundle + - '.. versionadded:: 1.11' # Messenger (Middleware / DoctrineBundle) - '.. versionadded:: 1.18' # Flex in setup/upgrade_minor.rst + - '.. versionadded:: 1.0.0' # Encore - '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 + - 'provides a ``loginUser()`` method to simulate logging in in your functional' - '.. code-block:: twig' + - '.. versionadded:: 3.6' # MonologBundle + - '// bin/console' - 'End to End Tests (E2E)' + - '.. code-block:: php' - '.. _`a feature to test applications using Mercure`: https://github.com/symfony/panther#creating-isolated-browsers-to-test-apps-using-mercure-or-websocket' + - '.. End to End Tests (E2E)' diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0586345396f..17cec7af7c3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,9 +1,9 @@ diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 73bbcca0235..c501b1a54fa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -26,7 +26,7 @@ jobs: - name: "Set-up PHP" uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.1 coverage: none tools: "composer:v2" @@ -90,7 +90,7 @@ jobs: - name: Set-up PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.1 coverage: none - name: Fetch branch from where the PR started diff --git a/README.markdown b/README.markdown index 8bd67bed4a3..79e6758c24e 100644 --- a/README.markdown +++ b/README.markdown @@ -27,7 +27,7 @@ We love contributors! For more information on how you can contribute, please rea the [Symfony Docs Contributing Guide](https://symfony.com/doc/current/contributing/documentation/overview.html) **Important**: use `4.4` branch as the base of your pull requests, unless you are -documenting a feature that was introduced *after* Symfony 4.4 (e.g. in Symfony 5.2). +documenting a feature that was introduced *after* Symfony 4.4 (e.g. in Symfony 5.4). Build Documentation Locally --------------------------- diff --git a/_build/composer.json b/_build/composer.json index fd7ec177c15..57b77fa5808 100644 --- a/_build/composer.json +++ b/_build/composer.json @@ -8,7 +8,10 @@ "preferred-install": { "*": "dist" }, - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "symfony/flex": true + } }, "require": { "php": ">=7.4", diff --git a/_build/maintainer_guide.rst b/_build/maintainer_guide.rst index d7eefad8edc..9758b4e7397 100644 --- a/_build/maintainer_guide.rst +++ b/_build/maintainer_guide.rst @@ -39,14 +39,14 @@ contributes again, it's OK to mention some of the minor issues to educate them. $ gh merge 11059 - Working on symfony/symfony-docs (branch 4.4) + Working on symfony/symfony-docs (branch 6.2) Merging Pull Request 11059: dmaicher/patch-3 ... # This is important!! Say NO to push the changes now Push the changes now? (Y/n) n - Now, push with: git push gh "4.4" refs/notes/github-comments + Now, push with: git push gh "6.2" refs/notes/github-comments # Now, open your editor and make the needed changes ... @@ -54,7 +54,7 @@ contributes again, it's OK to mention some of the minor issues to educate them. # Use "Minor reword", "Minor tweak", etc. as the commit message # now run the 'push' command shown above by 'gh' (it's different each time) - $ git push gh "4.4" refs/notes/github-comments + $ git push gh "6.2" refs/notes/github-comments Merging Pull Requests --------------------- diff --git a/_build/redirection_map b/_build/redirection_map index f0b726e546f..57cd82d276a 100644 --- a/_build/redirection_map +++ b/_build/redirection_map @@ -424,6 +424,7 @@ /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 @@ -474,6 +475,7 @@ /components/translation/usage /translation /components/translation/custom_formats https://github.com/symfony/translation /components/translation/custom_message_formatter https://github.com/symfony/translation +/components/notifier https://github.com/symfony/notifier /components/routing https://github.com/symfony/routing /doctrine/pdo_session_storage /session/database /doctrine/mongodb_session_storage /session/database @@ -505,7 +507,35 @@ /messenger/message-recorder /messenger/dispatch_after_current_bus /components/stopwatch https://github.com/symfony/stopwatch /service_container/3.3-di-changes https://symfony.com/doc/3.4/service_container/3.3-di-changes.html +/frontend/encore/shared-entry /frontend/encore/split-chunks +/frontend/encore/page-specific-assets /frontend/encore/simple-example#page-specific-javascript-or-css /testing/functional_tests_assertions /testing#testing-application-assertions /components https://symfony.com/components /components/index https://symfony.com/components /serializer/normalizers /components/serializer#normalizers +/logging/monolog_regex_based_excludes /logging/monolog_exclude_http_codes +/security/named_encoders /security/named_hashers +/components/inflector /components/string#inflector +/security/experimental_authenticators /security +/security/user_provider /security/user_providers +/security/reset_password /security/passwords#reset-password +/security/auth_providers /security#security-authenticators +/security/form_login /security#form-login +/security/form_login_setup /security#form-login +/security/json_login_setup /security#json-login +/security/named_hashers /security/passwords#named-password-hashers +/security/password_migration /security/passwords#security-password-migration +/security/acl https://github.com/symfony/acl-bundle/blob/main/src/Resources/doc/index.rst +/security/securing_services /security#securing-other-services +/security/authenticator_manager /security +/security/multiple_guard_authenticators /security/entry_point +/security/guard_authentication /security/custom_authenticator +/components/security/authentication /security#authenticating-users +/components/security/authorization /security#access-control-authorization +/components/security/firewall /security#the-firewall +/components/security/secure_tools /security/passwords +/components/security /security +/email /mailer +/frontend/assetic /frontend +/frontend/assetic/index /frontend +/controller/argument_value_resolver /controller/value_resolver diff --git a/_build/spelling_word_list.txt b/_build/spelling_word_list.txt index 70240ceb6d1..fa05ce9430e 100644 --- a/_build/spelling_word_list.txt +++ b/_build/spelling_word_list.txt @@ -3,7 +3,6 @@ Akamai analytics Ansi Ansible -Assetic async authenticator authenticators diff --git a/_images/components/console/completion.gif b/_images/components/console/completion.gif new file mode 100644 index 00000000000..18b3f5475c8 Binary files /dev/null and b/_images/components/console/completion.gif differ diff --git a/_images/components/console/cursor.gif b/_images/components/console/cursor.gif new file mode 100644 index 00000000000..71a74dd8637 Binary files /dev/null and b/_images/components/console/cursor.gif differ diff --git a/_images/components/console/debug_formatter.png b/_images/components/console/debug_formatter.png index 7482f39851f..4ba2c0c2b57 100644 Binary files a/_images/components/console/debug_formatter.png and b/_images/components/console/debug_formatter.png differ diff --git a/_images/components/console/process-helper-debug.png b/_images/components/console/process-helper-debug.png index 282e1336389..96c5c316739 100644 Binary files a/_images/components/console/process-helper-debug.png and b/_images/components/console/process-helper-debug.png differ diff --git a/_images/components/console/process-helper-error-debug.png b/_images/components/console/process-helper-error-debug.png index 8d1145478f2..48f6c7258d4 100644 Binary files a/_images/components/console/process-helper-error-debug.png and b/_images/components/console/process-helper-error-debug.png differ diff --git a/_images/components/console/process-helper-verbose.png b/_images/components/console/process-helper-verbose.png index c4c912e1433..abdff9812b0 100644 Binary files a/_images/components/console/process-helper-verbose.png and b/_images/components/console/process-helper-verbose.png differ diff --git a/_images/components/console/progress.png b/_images/components/console/progress.png deleted file mode 100644 index c126bff5252..00000000000 Binary files a/_images/components/console/progress.png and /dev/null differ diff --git a/_images/components/console/progressbar.gif b/_images/components/console/progressbar.gif index 6c80e6e897f..0746e399354 100644 Binary files a/_images/components/console/progressbar.gif and b/_images/components/console/progressbar.gif differ 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.png b/_images/components/workflow/blogpost.png index 38e29250eb1..b7f51eabb43 100644 Binary files a/_images/components/workflow/blogpost.png and b/_images/components/workflow/blogpost.png differ diff --git a/_images/components/workflow/blogpost_mermaid.png b/_images/components/workflow/blogpost_mermaid.png new file mode 100644 index 00000000000..7a4d3a57cfe Binary files /dev/null and b/_images/components/workflow/blogpost_mermaid.png differ diff --git a/_images/components/workflow/blogpost_puml.png b/_images/components/workflow/blogpost_puml.png index 14d45c8b40f..efe543a6f8e 100644 Binary files a/_images/components/workflow/blogpost_puml.png and b/_images/components/workflow/blogpost_puml.png differ diff --git a/_images/contributing/docs-github-create-pr.png b/_images/contributing/docs-github-create-pr.png index 29fe22f5dbd..43b6842ffc2 100644 Binary files a/_images/contributing/docs-github-create-pr.png and b/_images/contributing/docs-github-create-pr.png differ diff --git a/_images/contributing/docs-github-edit-page.png b/_images/contributing/docs-github-edit-page.png index c34f13f0889..9ea6c15421a 100644 Binary files a/_images/contributing/docs-github-edit-page.png and b/_images/contributing/docs-github-edit-page.png differ diff --git a/_images/contributing/docs-pull-request-change-base.png b/_images/contributing/docs-pull-request-change-base.png index d824e8ef1bc..791901b8ec6 100644 Binary files a/_images/contributing/docs-pull-request-change-base.png and b/_images/contributing/docs-pull-request-change-base.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 74128990e57..e1fba2bebf9 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/install/deprecations-in-profiler.png b/_images/install/deprecations-in-profiler.png index a8abcae32b7..3d3f9a98a4a 100644 Binary files a/_images/install/deprecations-in-profiler.png and b/_images/install/deprecations-in-profiler.png differ diff --git a/_images/notifier/microsoft_teams/message-card.png b/_images/notifier/microsoft_teams/message-card.png new file mode 100644 index 00000000000..05f505fb3e0 Binary files /dev/null and b/_images/notifier/microsoft_teams/message-card.png differ diff --git a/_images/notifier/microsoft_teams/message.png b/_images/notifier/microsoft_teams/message.png new file mode 100644 index 00000000000..5c4c7f11ed1 Binary files /dev/null and b/_images/notifier/microsoft_teams/message.png differ diff --git a/_images/notifier/slack/field-method.png b/_images/notifier/slack/field-method.png new file mode 100644 index 00000000000..d77a60e6a2e Binary files /dev/null and b/_images/notifier/slack/field-method.png differ diff --git a/_images/notifier/slack/message-reply.png b/_images/notifier/slack/message-reply.png new file mode 100644 index 00000000000..9a60e4573ab Binary files /dev/null and b/_images/notifier/slack/message-reply.png differ diff --git a/_images/notifier/slack/slack-footer.png b/_images/notifier/slack/slack-footer.png new file mode 100644 index 00000000000..a53952c78f6 Binary files /dev/null and b/_images/notifier/slack/slack-footer.png differ diff --git a/_images/notifier/slack/slack-header.png b/_images/notifier/slack/slack-header.png new file mode 100644 index 00000000000..a7caf915d8f Binary files /dev/null and b/_images/notifier/slack/slack-header.png differ diff --git a/_images/profiler/web-interface.png b/_images/profiler/web-interface.png index 2e6c6061892..2a1bc8a0650 100644 Binary files a/_images/profiler/web-interface.png 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 index 382950b6ef5..030953a17b1 100644 Binary files a/_images/quick_tour/no_routes_page.png and b/_images/quick_tour/no_routes_page.png differ diff --git a/_images/quick_tour/web_debug_toolbar.png b/_images/quick_tour/web_debug_toolbar.png deleted file mode 100644 index 465020380cb..00000000000 Binary files a/_images/quick_tour/web_debug_toolbar.png and /dev/null differ diff --git a/_images/rate_limiter/fixed_window.svg b/_images/rate_limiter/fixed_window.svg new file mode 100644 index 00000000000..83d5f6e79ac --- /dev/null +++ b/_images/rate_limiter/fixed_window.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + 10:00 + + + 10:30 + + + 11:00 + + + 11:30 + + + 12:00 + + + + + + + + 12:30 + + + 13:00 + + + + + + + + + + + + + + + + + + + + + + 1 hour window + + + 1 hour window + + + + + + 1 hour window + + + + + 13:15 + + + diff --git a/_images/rate_limiter/sliding_window.svg b/_images/rate_limiter/sliding_window.svg new file mode 100644 index 00000000000..2c565615441 --- /dev/null +++ b/_images/rate_limiter/sliding_window.svg @@ -0,0 +1,65 @@ + + + + + + + + + + 10:00 + + + 10:30 + + + 11:00 + + + 11:30 + + + 12:00 + + + + + + 12:30 + + + 13:00 + + + + + + + + + + + + + + + + + + + + + + 1 hour window + + + + + + 13:15 + + + + + + diff --git a/_images/rate_limiter/token_bucket.svg b/_images/rate_limiter/token_bucket.svg new file mode 100644 index 00000000000..29d6fc8f103 --- /dev/null +++ b/_images/rate_limiter/token_bucket.svg @@ -0,0 +1,83 @@ + + + + 10:00 + + + 10:30 + + + 11:00 + + + 11:30 + + + 12:00 + + + + + + + + 12:30 + + + 13:00 + + + + + + + + + + + + + + + + + + + + + + + + + + + 13:15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/release-process.jpg b/_images/release-process.jpg deleted file mode 100644 index 9868404b07f..00000000000 Binary files a/_images/release-process.jpg and /dev/null differ diff --git a/_images/security/anonymous_wdt.png b/_images/security/anonymous_wdt.png index 8dbf1cd8298..80736afce39 100644 Binary files a/_images/security/anonymous_wdt.png and b/_images/security/anonymous_wdt.png differ diff --git a/_images/security/login_link_email.png b/_images/security/login_link_email.png new file mode 100644 index 00000000000..8331b878f68 Binary files /dev/null and b/_images/security/login_link_email.png differ diff --git a/_images/security/profiler-badges.png b/_images/security/profiler-badges.png new file mode 100644 index 00000000000..a19f8539581 Binary files /dev/null and b/_images/security/profiler-badges.png differ diff --git a/_images/security/security_events.svg b/_images/security/security_events.svg new file mode 100644 index 00000000000..f1b93923da6 --- /dev/null +++ b/_images/security/security_events.svg @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/sources/README.md b/_images/sources/README.md index 8ca7538bf5d..a07bd5180fe 100644 --- a/_images/sources/README.md +++ b/_images/sources/README.md @@ -1,8 +1,8 @@ -How to Create Symfony Diagrams -============================== +How to Create Symfony Images +============================ -Creating the Diagram --------------------- +Creating Diagrams +----------------- * Use [Dia][1] as the diagramming application; * Use [PT Sans Narrow][2] as the only font in all diagrams (if possible, use @@ -21,8 +21,7 @@ Creating the Diagram In case of doubt, check the existing diagrams or ask to the [Symfony Documentation Team][3]. -Saving and Exporting the Diagram --------------------------------- +### Saving and Exporting the Diagram * Save the original diagram in `*.dia` format in `_images/sources/`; * Export the diagram to SVG format and save it in `_images/`. @@ -33,8 +32,7 @@ that transforms text into vector shapes (resulting file is larger in size, but it's truly portable because text is displayed the same even if you don't have some fonts installed). -Including the Diagram in the Symfony Docs ------------------------------------------ +### Including the Diagram in the Symfony Docs Use the following snippet to embed the diagram in the docs: @@ -44,21 +42,59 @@ Use the following snippet to embed the diagram in the docs: ``` -Reasoning ---------- +### Reasoning * Dia was chosen because it's one of the few applications which are free, open source and compatible with Linux, macOS and Windows. * Font, colors and line widths were chosen to be similar to the diagrams used in the best tech books. -Troubleshooting ---------------- +### Troubleshooting * On some macOS systems, Dia cannot be executed as a regular application and you must run the following console command instead: `export DISPLAY=:0 && /Applications/Dia.app/Contents/Resources/bin/dia` +Creating Console Screenshots +---------------------------- + +* Use [Asciinema][4] to record the console session locally: + + ``` + $ asciinema rec -c bash recording.cast + ``` +* Use `$ ` as the prompt in recordings. E.g. if you're using Bash, add the + following lines to your ``.bashrc``: + + ``` + if [ "$ASCIINEMA_REC" = "1" ]; then + PS1="\e[37m$ \e[0m" + fi + ``` +* Save the generated asciicast in `_images/sources/`. + +### Rendering the Recording + +Rendering the recording can be a difficult task. The [documentation team][3] +is always ready to help you with this task (e.g. you can open a PR with +only the asciicast file). + +* Use [agg][5] to generated a GIF file from the recording; +* Install the [JetBrains Mono][6] font; +* Use the ``_images/sources/ascii-render.sh`` file to call agg: + + ``` + AGG_PATH=/path/to/agg ./_images/sources/ascii-render.sh recording.cast --cols 45 --rows 20 + ``` + + This utility configures a predefined theme; +* Always configure `--cols`` (width) and ``--rows`` (height), try to use as + low as possible numbers. Do not exceed 70 columns; +* Save the generated GIF file in `_images/`. + [1]: http://dia-installer.de/ [2]: https://fonts.google.com/specimen/PT+Sans+Narrow [3]: https://symfony.com/doc/current/contributing/code/core_team.html +[4]: https://github.com/asciinema/asciinema +[5]: https://github.com/asciinema/agg +[6]: https://www.jetbrains.com/lp/mono/ diff --git a/_images/sources/ascii-render.sh b/_images/sources/ascii-render.sh new file mode 100755 index 00000000000..e72be572390 --- /dev/null +++ b/_images/sources/ascii-render.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env sh +case "$1" in + ''|help|-h) + echo "ansi-render.sh RECORDING [options]" + echo "" + echo " RECORDING: path to the .cast file generated by asciinema" + echo " [options]: optional options to be passed to agg" + ;; + *) + recording=$1 + extra_options= + if [ $# -gt 1 ]; then + shift + extra_options=$@ + fi + + # optionally, use this green color: 1f4631 + ${AGG_PATH:-agg} \ + --theme 18202a,f9fafb,f9fafb,ff7b72,7ee787,ffa657,79c0ff,d2a8ff,a5d6ff,f9fafb,8b949e,ff7b72,00c300,ffa657,79c0ff,d2a8ff,a5d6ff,f9fafb --line-height 1.6 \ + --font-family 'JetBrains Mono' \ + $extra_options \ + $recording $(echo $recording | sed "s/cast/gif/") + ;; +esac diff --git a/_images/sources/components/console/completion.cast b/_images/sources/components/console/completion.cast new file mode 100644 index 00000000000..c268863e9b0 --- /dev/null +++ b/_images/sources/components/console/completion.cast @@ -0,0 +1,37 @@ +{"version": 2, "width": 76, "height": 30, "timestamp": 1663253713, "env": {"SHELL": "/usr/bin/fish", "TERM": "st-256color"}} +[0.00798, "o", "\u001b[?2004h\u001b[90m$ \u001b[0m"] +[0.614685, "o", "b"] +[0.776549, "o", "i"] +[0.86682, "o", "n"] +[1.092426, "o", "/"] +[1.332671, "o", "c"] +[1.55068, "o", "o"] +[1.630651, "o", "n"] +[1.784584, "o", "s"] +[1.873108, "o", "o"] +[2.074652, "o", "l"] +[2.180433, "o", "e"] +[2.260475, "o", " "] +[2.696628, "o", "\u0007"] +[2.947263, "o", "\r\nabout debug:event-dispatcher\r\nassets:install debug:router\r\ncache:clear help\r\ncache:pool:clear lint:container\r\ncache:pool:delete lint:yaml\r\ncache:pool:list list\r\ncache:pool:prune router:match\r\ncache:warmup secrets:decrypt-to-local\r\ncompletion secrets:encrypt-from-local\r\nconfig:dump-reference secrets:generate-keys\r\ndebug:autowiring secrets:list\r\ndebug:config secrets:remove\r\ndebug:container secrets:set\r\ndebug:dotenv \r\n\u001b[37m$ \u001b[0mbin/console "] +[3.614479, "o", "s"] +[3.802449, "o", "e"] +[4.205631, "o", "\u0007crets:"] +[4.520435, "o", "r"] +[4.598031, "o", "e"] +[5.026287, "o", "move "] +[5.47041, "o", "\u0007SOME_"] +[5.673941, "o", "\u0007"] +[6.024086, "o", "\r\nSOME_OTHER_SECRET SOME_SECRET \r\n\u001b[37m$ \u001b[0mbin/console secrets:remove SOME_"] +[6.770627, "o", "O"] +[7.14335, "o", "THER_SECRET "] +[7.724482, "o", "\r\n\u001b[?2004l\r"] +[7.776657, "o", "\r\n"] +[7.779108, "o", "\u001b[30;42m \u001b[39;49m\r\n\u001b[30;42m [OK] Secret \"SOME_OTHER_SECRET\" removed from \"config/secrets/dev/\". \u001b[39;49m\r\n\u001b[30;42m \u001b[39;49m\r\n\r\n"] +[7.782993, "o", "\u001b[?2004h\u001b[37m$ \u001b[0m"] +[9.214537, "o", "e"] +[9.522429, "o", "x"] +[9.690371, "o", "i"] +[9.85446, "o", "t"] +[10.292412, "o", "\r\n\u001b[?2004l\r"] +[10.292526, "o", "exit\r\n"] diff --git a/_images/sources/components/console/cursor.cast b/_images/sources/components/console/cursor.cast new file mode 100644 index 00000000000..be2f2f6c351 --- /dev/null +++ b/_images/sources/components/console/cursor.cast @@ -0,0 +1,49 @@ +{"version": 2, "width": 191, "height": 30, "timestamp": 1663251833, "env": {"SHELL": "/usr/bin/fish", "TERM": "st-256color"}} +[0.007941, "o", "\u001b[?2004h\u001b[90m$ \u001b[0m"] +[0.566363, "o", "c"] +[0.643353, "o", "l"] +[0.762325, "o", "e"] +[0.952363, "o", "a"] +[0.995878, "o", "r"] +[1.107784, "o", "\r\n\u001b[?2004l\r"] +[1.109766, "o", "\u001b[H\u001b[2J"] +[1.109946, "o", "\u001b[?2004h\u001b[30m$ \u001b[0m"] +[1.653461, "o", "p"] +[1.772323, "o", "h"] +[1.856444, "o", "p"] +[1.980339, "o", " "] +[2.15827, "o", "c"] +[2.273242, "o", "u"] +[2.402231, "o", "r"] +[2.563066, "o", "s"] +[2.760266, "o", "o"] +[2.900252, "o", "r"] +[3.020537, "o", "."] +[3.316404, "o", "p"] +[3.403213, "o", "h"] +[3.483391, "o", "p"] +[3.820273, "o", "\r\n\u001b[?2004l\r"] +[3.845697, "o", "\u001b[6;9H#"] +[4.045942, "o", "\u001b[8;9H#"] +[4.246327, "o", "\u001b[8;2H#####"] +[4.446737, "o", "\u001b[2;9H#######"] +[4.647128, "o", "\u001b[7;7H#"] +[4.84749, "o", "\u001b[3;9H#"] +[5.047857, "o", "\u001b[7;9H#"] +[5.248246, "o", "\u001b[4;9H#"] +[5.448622, "o", "\u001b[2;2H#####"] +[5.648999, "o", "\u001b[3;7H#"] +[5.849378, "o", "\u001b[5;9H#####"] +[6.049711, "o", "\u001b[3;1H#"] +[6.250118, "o", "\u001b[7;1H#"] +[6.45056, "o", "\u001b[5;2H#####"] +[6.650897, "o", "\u001b[4;1H#"] +[6.851281, "o", "\u001b[6;7H#"] +[7.051644, "o", "\u001b[9;1H"] +[7.058802, "o", "\u001b[?2004h\u001b[30m$ \u001b[0m"] +[7.657612, "o", "e"] +[7.846956, "o", "x"] +[7.949451, "o", "i"] +[8.0893, "o", "t"] +[8.201144, "o", "\r\n\u001b[?2004l\r"] +[8.201227, "o", "exit\r\n"] diff --git a/_images/sources/components/console/progress.cast b/_images/sources/components/console/progress.cast new file mode 100644 index 00000000000..9c5244b37e2 --- /dev/null +++ b/_images/sources/components/console/progress.cast @@ -0,0 +1,57 @@ +{"version": 2, "width": 191, "height": 17, "timestamp": 1663423221, "env": {"SHELL": "/usr/bin/fish", "TERM": "st-256color"}} +[0.008171, "o", "\u001b[?2004h\u001b[90m$ \u001b[0m"] +[0.385858, "o", "p"] +[0.577979, "o", "h"] +[0.768282, "o", "p"] +[0.96433, "o", " "] +[1.133645, "o", "p"] +[1.262693, "o", "r"] +[1.385832, "o", "o"] +[1.476876, "o", "g"] +[1.652322, "o", "r"] +[1.722357, "o", "e"] +[1.935395, "o", "s"] +[2.083915, "o", "s"] +[2.200109, "o", "."] +[2.403686, "o", "p"] +[2.510201, "o", "h"] +[2.602756, "o", "p"] +[2.909974, "o", "\r\n\u001b[?2004l\r"] +[2.935647, "o", "\u001b[34m Starting the demo... fingers crossed \u001b[39m\r\n 0/15 \u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 0%\r\n  < 1 sec 4.0 MiB"] +[3.418022, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K"] +[3.419196, "o", "\u001b[34m Starting the demo... fingers crossed \u001b[39m\r\n 2/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 13%\r\n  < 1 sec 6.0 MiB"] +[3.66102, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G"] +[3.661071, "o", "\u001b[2K"] +[3.661731, "o", "\u001b[34m Starting the demo... fingers crossed \u001b[39m\r\n 3/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 20%\r\n  5 secs 6.0 MiB"] +[4.143554, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K"] +[4.14385, "o", "\u001b[34m Starting the demo... fingers crossed \u001b[39m\r\n 5/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 33%\r\n  3 secs 6.5 MiB"] +[4.385367, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K"] +[4.38612, "o", "\u001b[34m Looks good to me... \u001b[39m\r\n 6/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 40%\r\n  3 secs 7.1 MiB"] +[4.868053, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K"] +[4.86852, "o", "\u001b[34m Looks good to me... \u001b[39m\r\n 8/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 53%\r\n  4 secs 8.1 MiB"] +[5.110341, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K"] +[5.11133, "o", "\u001b[34m Looks good to me... \u001b[39m\r\n 9/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 60%\r\n  3 secs 8.6 MiB"] +[5.593851, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G"] +[5.593924, "o", "\u001b[2K"] +[5.594818, "o", "\u001b[34m Looks good to me... \u001b[39m\r\n11/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 73%\r\n  4 secs 9.6 MiB"] +[5.836301, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K"] +[5.836831, "o", "\u001b[34m Looks good to me... \u001b[39m\r\n12/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m\u001b[41m \u001b[49m 80%\r\n  4 secs 10.1 MiB"] +[6.31877, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K\u001b[1A"] +[6.318814, "o", "\u001b[1G\u001b[2K"] +[6.319403, "o", "\u001b[34m Looks good to me... \u001b[39m\r\n14/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[32;41m\u001b[39;49m\u001b[41m \u001b[49m 93%\r\n  3 secs 11.1 MiB"] +[6.561359, "o", "\u001b[1G\u001b[2K\u001b[1A"] +[6.561561, "o", "\u001b[1G\u001b[2K\u001b[1A\u001b[1G\u001b[2K"] +[6.562504, "o", "\u001b[34m Looks good to me... \u001b[39m\r\n15/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m 100%\r\n  4 secs 11.6 MiB"] +[6.563772, "o", "\u001b[1G"] +[6.563824, "o", "\u001b[2K\u001b[1A"] +[6.563875, "o", "\u001b[1G\u001b[2K"] +[6.563926, "o", "\u001b[1A\u001b[1G\u001b[2K"] +[6.564766, "o", "\u001b[34m Thanks bye! \u001b[39m\r\n15/15 \u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m\u001b[42m \u001b[49m 100%\r\n  4 secs 11.6 MiB"] +[6.564805, "o", "\r\n\r\n"] +[6.570516, "o", "\u001b[?2004h"] +[6.570537, "o", "\u001b[90m$ \u001b[0m"] +[8.441927, "o", "e"] +[8.646449, "o", "x"] +[8.76668, "o", "i"] +[8.897799, "o", "t"] +[9.091614, "o", "\r\n\u001b[?2004l\rexit\r\n"] diff --git a/_images/sources/rate_limiter/fixed_window.dia b/_images/sources/rate_limiter/fixed_window.dia new file mode 100644 index 00000000000..16282a2dcce Binary files /dev/null and b/_images/sources/rate_limiter/fixed_window.dia differ diff --git a/_images/sources/rate_limiter/sliding_window.dia b/_images/sources/rate_limiter/sliding_window.dia new file mode 100644 index 00000000000..e16275d8995 Binary files /dev/null and b/_images/sources/rate_limiter/sliding_window.dia differ diff --git a/_images/sources/rate_limiter/token_bucket.dia b/_images/sources/rate_limiter/token_bucket.dia new file mode 100644 index 00000000000..16761971337 Binary files /dev/null and b/_images/sources/rate_limiter/token_bucket.dia differ diff --git a/_images/sources/security/security_events.dia b/_images/sources/security/security_events.dia new file mode 100644 index 00000000000..0a8afa73179 Binary files /dev/null and b/_images/sources/security/security_events.dia differ diff --git a/best_practices.rst b/best_practices.rst index 865f7549fa3..bc2269b4236 100644 --- a/best_practices.rst +++ b/best_practices.rst @@ -196,17 +196,20 @@ 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 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Use Attributes 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 +Doctrine supports several metadata formats, but it's recommended to use PHP +attributes because they are by far the most convenient and agile way of setting up and looking for mapping information. +If your PHP version doesn't support attributes yet, use annotations, which is +similar but requires installing some extra dependencies in your project. + Controllers ----------- @@ -225,27 +228,20 @@ 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 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Use Attributes or Annotations to Configure Routing, Caching and Security +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -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. +Using attributes or 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. Use Dependency Injection to Get Services ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you extend the base ``AbstractController``, you can only access to the most +If you extend the base ``AbstractController``, you can only get access to the most common services (e.g ``twig``, ``router``, ``doctrine``, etc.), directly from the -container via ``$this->container->get()`` or ``$this->get()``. +container via ``$this->container->get()``. Instead, you must use dependency injection to fetch services by :ref:`type-hinting action method arguments ` or constructor arguments. @@ -287,7 +283,7 @@ 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. +controllers simplifies the code and maintenance of the controllers. Add Form Buttons in Templates ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -369,14 +365,14 @@ 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``. +Currently, the default auto hasher is ``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. +inside the ``#[Security]`` attribute. Web Assets ---------- @@ -385,7 +381,7 @@ Use Webpack Encore to Process Web Assets ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Web assets are things like CSS, JavaScript and image files that make the -frontend of your site looks and works great. `Webpack`_ is the leading JavaScript +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 diff --git a/bundles.rst b/bundles.rst index bf5a144d4ce..904f2d3f497 100644 --- a/bundles.rst +++ b/bundles.rst @@ -28,7 +28,6 @@ file:: 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' @@ -47,20 +46,29 @@ Creating a Bundle 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 an example name that should be replaced by some "vendor" name that represents you or your -organization (e.g. ABCTestBundle for some company named ``ABC``). +organization (e.g. AbcTestBundle for some company named ``Abc``). -Start by creating a ``src/Acme/TestBundle/`` directory and adding a new file -called ``AcmeTestBundle.php``:: +Start by adding creating a new class called ``AcmeTestBundle``:: - // src/Acme/TestBundle/AcmeTestBundle.php - namespace App\Acme\TestBundle; + // src/AcmeTestBundle.php + namespace Acme\TestBundle; - use Symfony\Component\HttpKernel\Bundle\Bundle; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; - class AcmeTestBundle extends Bundle + class AcmeTestBundle extends AbstractBundle { } +.. versionadded:: 6.1 + + The :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` was + introduced in Symfony 6.1. + +.. caution:: + + If your bundle must be compatible with previous Symfony versions you have to + extend from the :class:`Symfony\\Component\\HttpKernel\\Bundle\\Bundle` instead. + .. tip:: The name AcmeTestBundle follows the standard @@ -75,7 +83,7 @@ of the bundle. Now that you've created the bundle, enable it:: // config/bundles.php return [ // ... - App\Acme\TestBundle\AcmeTestBundle::class => ['all' => true], + Acme\TestBundle\AcmeTestBundle::class => ['all' => true], ]; And while it doesn't do anything yet, AcmeTestBundle is now ready to be used. @@ -87,35 +95,68 @@ 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``). - -``DependencyInjection/`` - Holds certain Dependency Injection Extension classes, which may import service - configuration, register compiler passes or more (this directory is not - necessary). - -``Resources/config/`` +``src/`` + Contains all PHP classes related to the bundle logic (e.g. ``Controller/RandomController.php``). + +``config/`` Houses configuration, including routing configuration (e.g. ``routing.yaml``). -``Resources/views/`` - Holds templates organized by controller name (e.g. ``Random/index.html.twig``). +``templates/`` + Holds templates organized by controller name (e.g. ``random/index.html.twig``). + +``translations/`` + Holds translations organized by domain and locale (e.g. ``AcmeTestBundle.en.xlf``). -``Resources/public/`` +``public/`` Contains web assets (images, stylesheets, etc) and is copied or symbolically linked into the project ``public/`` directory via the ``assets:install`` console command. -``Tests/`` +``assets/`` + Contains JavaScript, CSS, images and other assets related to the bundle that + are not in ``public/`` (e.g. stimulus controllers) + +``tests/`` Holds all tests for the bundle. -A bundle can be as small or large as the feature it implements. It contains -only the files you need and nothing else. +.. caution:: + + The recommended bundle structure was changed in Symfony 5, read the + `Symfony 4.4 bundle documentation`_ for information about the old + structure. + + When using the new ``AbstractBundle`` class, the bundle defaults to the + new structure. Override the ``Bundle::getPath()`` method to change to + the old structure:: + + class AcmeTestBundle extends AbstractBundle + { + public function getPath(): string + { + return __DIR__; + } + } + +.. tip:: -As you move through the guides, you'll learn how to persist objects to a -database, create and validate forms, create translations for your application, -write tests and much more. Each of these has their own place and role within -the bundle. + 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. As the main class is located in the ``src/`` directory of the bundle: + + .. code-block:: json + + { + "autoload": { + "psr-4": { + "Acme\\TestBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Acme\\TestBundle\\Tests\\": "tests/" + } + } + } Learn more ---------- @@ -127,3 +168,5 @@ Learn more * :doc:`/bundles/prepend_extension` .. _`third-party bundles`: https://github.com/search?q=topic%3Asymfony-bundle&type=Repositories +.. _`Symfony 4.4 bundle documentation`: https://symfony.com/doc/4.4/bundles.html#bundle-directory-structure +.. _`PSR-4`: https://www.php-fig.org/psr/psr-4/ diff --git a/bundles/best_practices.rst b/bundles/best_practices.rst index addc59014ba..4ef81080637 100644 --- a/bundles/best_practices.rst +++ b/bundles/best_practices.rst @@ -22,8 +22,9 @@ 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 rules: +A namespace becomes a bundle as soon as you add "a bundle class" to it (which is +a class that extends :class:`Symfony\\Component\\HttpKernel\\Bundle\\Bundle`). +The 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); @@ -82,11 +83,6 @@ The following is the recommended directory structure of an AcmeBlogBundle: ├── LICENSE └── README.md -.. versionadded:: 4.4 - - This directory convention was introduced in Symfony 4.4 and can be used only - when requiring ``symfony/http-kernel`` 4.4 or superior. - This directory structure requires to configure the bundle path to its root directory as follows:: @@ -127,8 +123,8 @@ Type Directory Commands ``src/Command/`` Controllers ``src/Controller/`` Service Container Extensions ``src/DependencyInjection/`` -Doctrine ORM entities (when not using annotations) ``src/Entity/`` -Doctrine ODM documents (when not using annotations) ``src/Document/`` +Doctrine ORM entities ``src/Entity/`` +Doctrine ODM documents ``src/Document/`` Event Listeners ``src/EventListener/`` Configuration (routes, services, etc.) ``config/`` Web Assets (CSS, JS, images) ``public/`` @@ -166,6 +162,15 @@ standard Symfony autoloading instead. A bundle should also not embed third-party libraries written in JavaScript, CSS or any other language. +Doctrine Entities/Documents +--------------------------- + +If the bundle includes Doctrine ORM entities and/or ODM documents, it's +recommended to define their mapping using XML files stored in +``Resources/config/doctrine/``. This allows to override that mapping using the +:doc:`standard Symfony mechanism to override bundle parts `. +This is not possible when using annotations/attributes to define the mapping. + Tests ----- @@ -198,15 +203,15 @@ A bundle should at least test: * All supported major Symfony versions (e.g. both ``4.x`` and ``5.x`` if support is claimed for both). -Thus, a bundle supporting PHP 7.3, 7.4 and 8.0, and Symfony 3.4 and 4.x should +Thus, a bundle supporting PHP 7.3, 7.4 and 8.0, and Symfony 4.4 and 5.x should have at least this test matrix: =========== =============== =================== PHP version Symfony version Composer flags =========== =============== =================== -7.3 ``3.*`` ``--prefer-lowest`` -7.4 ``4.*`` -8.0 ``4.*`` +7.3 ``4.*`` ``--prefer-lowest`` +7.4 ``5.*`` +8.0 ``5.*`` =========== =============== =================== .. tip:: @@ -232,6 +237,7 @@ with Symfony Flex to install a specific Symfony version: # composer config extra.symfony.require "5.*" # install Symfony Flex in the CI environment + composer global config --no-plugins allow-plugins.symfony/flex true composer global require --no-progress --no-scripts --no-plugins symfony/flex # install the dependencies (using --prefer-dist and --no-progress is diff --git a/bundles/configuration.rst b/bundles/configuration.rst index 41c34ee7bbc..d8c7a44d7bd 100644 --- a/bundles/configuration.rst +++ b/bundles/configuration.rst @@ -44,12 +44,10 @@ as integration of other related components: .. code-block:: php // config/packages/framework.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use Symfony\Config\FrameworkConfig; - return static function (ContainerConfigurator $container) { - $container->extension('framework', [ - 'form' => true, - ]); + return static function (FrameworkConfig $framework) { + $framework->form()->enabled(true); }; Using the Bundle Extension @@ -89,15 +87,12 @@ can add some configuration that looks like this: .. code-block:: php // config/packages/acme_social.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - return static function (ContainerConfigurator $container) { - $container->extension('acme_social', [ - 'twitter' => [ - 'client_id' => 123, - 'client_secret' => 'your_secret', - ], - ]); + use Symfony\Config\AcmeSocialConfig; + + return static function (AcmeSocialConfig $acmeSocial) { + $acmeSocial->twitter() + ->clientId(123) + ->clientSecret('your_secret'); }; The basic idea is that instead of having the user override individual @@ -211,10 +206,6 @@ The ``Configuration`` class to handle the sample configuration looks like:: } } -.. deprecated:: 4.2 - - Not passing the root node name to ``TreeBuilder`` was deprecated in Symfony 4.2. - .. seealso:: The ``Configuration`` class can be much more complicated than shown here, @@ -328,6 +319,88 @@ In your extension, you can load this and dynamically set its arguments:: // ... now use the flat $config array } +Using the Bundle Class +---------------------- + +.. versionadded:: 6.1 + + The ``AbstractBundle`` class was introduced in Symfony 6.1. + +Instead of creating an extension and configuration class, you can also +extend :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` to +add this logic to the bundle class directly:: + + // src/AcmeSocialBundle.php + namespace Acme\SocialBundle; + + use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + + class AcmeSocialBundle extends AbstractBundle + { + public function configure(DefinitionConfigurator $definition): void + { + $definition->rootNode() + ->children() + ->arrayNode('twitter') + ->children() + ->integerNode('client_id')->end() + ->scalarNode('client_secret')->end() + ->end() + ->end() // twitter + ->end() + ; + } + + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + // Contrary to the Extension class, the "$config" variable is already merged + // and processed. You can use it directly to configure the service container. + $container->services() + ->get('acme.social.twitter_client') + ->arg(0, $config['twitter']['client_id']) + ->arg(1, $config['twitter']['client_secret']) + ; + } + } + +.. note:: + + The ``configure()`` and ``loadExtension()`` methods are called only at compile time. + +.. tip:: + + The ``AbstractBundle::configure()`` method also allows to import the + configuration definition from one or more files:: + + // src/AcmeSocialBundle.php + + // ... + class AcmeSocialBundle extends AbstractBundle + { + public function configure(DefinitionConfigurator $definition): void + { + $definition->import('../config/definition.php'); + // you can also use glob patterns + //$definition->import('../config/definition/*.php'); + } + + // ... + } + + .. code-block:: php + + // config/definition.php + use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; + + return static function (DefinitionConfigurator $definition) { + $definition->rootNode() + ->children() + ->scalarNode('foo')->defaultValue('bar')->end() + ->end() + ; + }; + Modifying the Configuration of Another Bundle --------------------------------------------- diff --git a/bundles/extension.rst b/bundles/extension.rst index edbcb5cd270..40100f30beb 100644 --- a/bundles/extension.rst +++ b/bundles/extension.rst @@ -30,7 +30,7 @@ follow these conventions (but later you'll learn how to skip them if needed): This is how the extension of an AcmeHelloBundle should look like:: - // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php + // src/DependencyInjection/AcmeHelloExtension.php namespace Acme\HelloBundle\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -87,7 +87,7 @@ but it is more common if you put these definitions in a configuration file (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:: +``config/`` directory of your bundle, your ``load()`` method looks like:: use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -97,7 +97,7 @@ For instance, assume you have a file called ``services.xml`` in the { $loader = new XmlFileLoader( $container, - new FileLocator(__DIR__.'/../Resources/config') + new FileLocator(__DIR__.'/../../config') ); $loader->load('services.xml'); } @@ -111,6 +111,57 @@ The Extension is also the class that handles the configuration for that particular bundle (e.g. the configuration in ``config/packages/.yaml``). To read more about it, see the ":doc:`/bundles/configuration`" article. +Loading Services directly in your Bundle class +---------------------------------------------- + +.. versionadded:: 6.1 + + The ``AbstractBundle`` class was introduced in Symfony 6.1. + +Alternatively, you can define and load services configuration directly in a +bundle class instead of creating a specific ``Extension`` class. You can do +this by extending from :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` +and defining the :method:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle::loadExtension` +method:: + + // ... + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + + class AcmeHelloBundle extends AbstractBundle + { + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + // load an XML, PHP or Yaml file + $container->import('../config/services.xml'); + + // you can also add or replace parameters and services + $container->parameters() + ->set('acme_hello.phrase', $config['phrase']) + ; + + if ($config['scream']) { + $container->services() + ->get('acme_hello.printer') + ->class(ScreamingPrinter::class) + ; + } + } + } + +This method works similar to the ``Extension::load()`` method, but it uses +a new API to define and import service configuration. + +.. note:: + + Contrary to the ``$configs`` parameter in ``Extension::load()``, the + ``$config`` parameter is already merged and processed by the + ``AbstractBundle``. + +.. note:: + + The ``loadExtension()`` is called only at compile time. + Adding Classes to Compile ------------------------- diff --git a/bundles/override.rst b/bundles/override.rst index 6cf3d37c386..c7e5abceaad 100644 --- a/bundles/override.rst +++ b/bundles/override.rst @@ -12,7 +12,7 @@ features of a bundle. 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. ``@FooBundle/Resources/config/services.xml``) + use logical paths in your bundles (e.g. ``@FooBundle/config/services.xml``) and call the :ref:`locateResource() method ` to turn them into physical paths when needed. @@ -23,12 +23,12 @@ Templates 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 +must use the same name and path (relative to ``/templates/``) 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`` +For example, to override the ``templates/registration/confirmed.html.twig`` +template from the AcmeUserBundle, create this template: +``/templates/bundles/AcmeUserBundle/registration/confirmed.html.twig`` .. caution:: @@ -43,9 +43,9 @@ extend from the original template, not from the overridden one: .. code-block:: twig - {# templates/bundles/FOSUserBundle/Registration/confirmed.html.twig #} + {# templates/bundles/AcmeUserBundle/registration/confirmed.html.twig #} {# the special '!' prefix avoids errors when extending from an overridden template #} - {% extends "@!FOSUser/Registration/confirmed.html.twig" %} + {% extends "@!AcmeUser/registration/confirmed.html.twig" %} {% block some_block %} ... @@ -173,7 +173,7 @@ 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. +``translations/AcmeUserBundle.es.yaml`` file of the AcmeUserBundle, +create a ``/translations/AcmeUserBundle.es.yaml`` file. .. _`the Doctrine documentation`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/inheritance-mapping.html#overrides diff --git a/bundles/prepend_extension.rst b/bundles/prepend_extension.rst index fe551f31083..2a11c832a73 100644 --- a/bundles/prepend_extension.rst +++ b/bundles/prepend_extension.rst @@ -65,18 +65,16 @@ in case a specific other bundle is not registered:: // disable AcmeGoodbyeBundle in bundles $config = ['use_acme_goodbye' => false]; foreach ($container->getExtensions() as $name => $extension) { - switch ($name) { - case 'acme_something': - case 'acme_other': - // set use_acme_goodbye to false in the config of - // acme_something and acme_other - // - // note that if the user manually configured - // use_acme_goodbye to true in config/services.yaml - // then the setting would in the end be true and not false - $container->prependExtensionConfig($name, $config); - break; - } + match ($name) { + // set use_acme_goodbye to false in the config of + // acme_something and acme_other + // + // note that if the user manually configured + // use_acme_goodbye to true in config/services.yaml + // then the setting would in the end be true and not false + 'acme_something', 'acme_other' => $container->prependExtensionConfig($name, $config), + default => null + }; } } @@ -157,6 +155,45 @@ registered and the ``entity_manager_name`` setting for ``acme_hello`` is set to ]); }; +Prepending Extension in the Bundle Class +---------------------------------------- + +.. versionadded:: 6.1 + + The ``AbstractBundle`` class was introduced in Symfony 6.1. + +You can also append or prepend extension configuration directly in your +Bundle class if you extend from the :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` +class and define the :method:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle::prependExtension` +method:: + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + + class FooBundle extends AbstractBundle + { + public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void + { + // prepend + $builder->prependExtensionConfig('framework', [ + 'cache' => ['prefix_seed' => 'foo/bar'], + ]); + + // append + $container->extension('framework', [ + 'cache' => ['prefix_seed' => 'foo/bar'], + ]); + + // append from file + $container->import('../config/packages/cache.php'); + } + } + +.. note:: + + The ``prependExtension()`` method, like ``prepend()``, is called only at compile time. + More than one Bundle using PrependExtensionInterface ---------------------------------------------------- diff --git a/cache.rst b/cache.rst index 73766284f21..ab02c8c8272 100644 --- a/cache.rst +++ b/cache.rst @@ -27,13 +27,9 @@ The following example shows a typical usage of the cache:: // ... and to remove the cache key $pool->delete('my_cache_key'); -Symfony supports Cache Contracts, PSR-6/16 and Doctrine Cache interfaces. +Symfony supports Cache Contracts and PSR-6/16 interfaces. You can read more about these at the :doc:`component documentation `. -.. versionadded:: 4.2 - - The cache contracts were introduced in Symfony 4.2. - .. _cache-configuration-with-frameworkbundle: Configuring Cache with FrameworkBundle @@ -90,27 +86,25 @@ adapter (template) they use by using the ``app`` and ``system`` key like: .. code-block:: php // config/packages/cache.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use Symfony\Config\FrameworkConfig; - return static function (ContainerConfigurator $container) { - $container->extension('framework', [ - 'cache' => [ - 'app' => 'cache.adapter.filesystem', - 'system' => 'cache.adapter.system', - ], - ]); + return static function (FrameworkConfig $framework) { + $framework->cache() + ->app('cache.adapter.filesystem') + ->system('cache.adapter.system') + ; }; 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 ` +* :ref:`cache.adapter.redis_tag_aware ` (Redis adapter optimized to work with tags) Some of these adapters could be configured via shortcuts. Using these shortcuts will create pools with service IDs that follow the pattern ``cache.[type]``. @@ -124,8 +118,6 @@ will create pools with service IDs that follow the pattern ``cache.[type]``. cache: directory: '%kernel.cache_dir%/pools' # Only used with cache.adapter.filesystem - # service: cache.doctrine - default_doctrine_provider: 'app.doctrine_cache' # service: cache.psr6 default_psr6_provider: 'app.my_psr6_service' # service: cache.redis @@ -149,7 +141,6 @@ will create pools with service IDs that follow the pattern ``cache.[type]``. > extension('framework', [ - 'cache' => [ - // Only used with cache.adapter.filesystem - 'directory' => '%kernel.cache_dir%/pools', - - // Service: cache.doctrine - 'default_doctrine_provider' => 'app.doctrine_cache', - // Service: cache.psr6 - 'default_psr6_provider' => 'app.my_psr6_service', - // Service: cache.redis - 'default_redis_provider' => 'redis://localhost', - // Service: cache.memcached - 'default_memcached_provider' => 'memcached://localhost', - // Service: cache.pdo - 'default_pdo_provider' => 'doctrine.dbal.default_connection', - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->cache() + // Only used with cache.adapter.filesystem + ->directory('%kernel.cache_dir%/pools') + // Service: cache.psr6 + ->defaultPsr6Provider('app.my_psr6_service') + // Service: cache.redis + ->defaultRedisProvider('redis://localhost') + // Service: cache.memcached + ->defaultMemcachedProvider('memcached://localhost') + // Service: cache.pdo + ->defaultPdoProvider('doctrine.dbal.default_connection') + ; }; +.. _cache-create-pools: + Creating Custom (Namespaced) Pools ---------------------------------- @@ -273,46 +260,35 @@ You can also create more customized pools: .. code-block:: php // config/packages/cache.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - return static function (ContainerConfigurator $container) { - $container->extension('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', - ], - - // uses the default_memcached_provider from above - 'acme.cache' => [ - 'adapter' => 'cache.adapter.memcached', - ], - - // 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, - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $cache = $framework->cache(); + $cache->defaultMemcachedProvider('memcached://localhost'); + + // creates a "custom_thing.cache" service + // autowireable via "CacheInterface $customThingCache" + // uses the "app" cache configuration + $cache->pool('custom_thing.cache') + ->adapters(['cache.app']); + + // creates a "my_cache_pool" service + // autowireable via "CacheInterface $myCachePool" + $cache->pool('my_cache_pool') + ->adapters(['cache.adapter.filesystem']); + + // uses the default_memcached_provider from above + $cache->pool('acme.cache') + ->adapters(['cache.adapter.memcached']); + + // control adapter's configuration + $cache->pool('foobar.cache') + ->adapters(['cache.adapter.memcached']) + ->provider('memcached://user:password@example.com'); + + $cache->pool('short_cache') + ->adapters(['foobar.cache']) + ->defaultLifetime(60); }; Each pool manages a set of independent cache keys: keys from different pools @@ -385,8 +361,8 @@ with either :class:`Symfony\\Contracts\\Cache\\CacheInterface` or // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $container) { - $container->services() + return function(ContainerConfigurator $configurator) { + $configurator->services() // ... ->set('app.cache.adapter.redis') @@ -463,29 +439,23 @@ and use that when configuring the pool. namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Component\Cache\Adapter\RedisAdapter; - - return static function (ContainerConfigurator $container) { - $container->extension('framework', [ - 'cache' => [ - 'pools' => [ - 'cache.my_redis' => [ - 'adapter' => 'cache.adapter.redis', - 'provider' => 'app.my_custom_redis_provider', - ], - ], - ], - ]); - - $container->services() - ->set('app.my_custom_redis_provider', \Redis::class) - ->factory([RedisAdapter::class, 'createConnection']) - ->args([ - 'redis://localhost', - [ - 'retry_interval' => 2, - 'timeout' => 10, - ] - ]) + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container, FrameworkConfig $framework) { + $framework->cache() + ->pool('cache.my_redis') + ->adapters(['cache.adapter.redis']) + ->provider('app.my_custom_redis_provider'); + + + $container->register('app.my_custom_redis_provider', \Redis::class) + ->setFactory([RedisAdapter::class, 'createConnection']) + ->addArgument('redis://localhost') + ->addArgument([ + 'retry_interval' => 2, + 'timeout' => 10 + ]) ; }; @@ -508,10 +478,6 @@ If an error happens when storing an item in a pool, Symfony stores it in the other pools and no exception is thrown. Later, when the item is retrieved, Symfony stores the item automatically in all the missing pools. -.. versionadded:: 4.4 - - Support for configuring a chain using ``framework.cache.pools`` was introduced in Symfony 4.4. - .. configuration-block:: .. code-block:: yaml @@ -554,23 +520,18 @@ Symfony stores the item automatically in all the missing pools. .. code-block:: php // config/packages/cache.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - return static function (ContainerConfigurator $container) { - $container->extension('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'], - ], - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->cache() + ->pool('my_cache_pool') + ->defaultLifetime(31536000) // One year + ->adapters([ + 'cache.adapter.array', + 'cache.adapter.apcu', + ['name' => 'cache.adapter.redis', 'provider' => 'redis://user:password@example.com'], + ]) + ; }; Using Cache Tags @@ -653,19 +614,14 @@ to enable this feature. This could be added by using the following configuration .. code-block:: php // config/packages/cache.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use Symfony\Config\FrameworkConfig; - return static function (ContainerConfigurator $container) { - $container->extension('framework', [ - 'cache' => [ - 'pools' => [ - 'my_cache_pool' => [ - 'adapter' => 'cache.adapter.redis', - 'tags' => true, - ], - ], - ], - ]); + return static function (FrameworkConfig $framework) { + $framework->cache() + ->pool('my_cache_pool') + ->tags(true) + ->adapters(['cache.adapter.redis']) + ; }; Tags are stored in the same pool by default. This is good in most scenarios. But @@ -712,22 +668,19 @@ achieved by specifying the adapter. .. code-block:: php // config/packages/cache.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->cache() + ->pool('my_cache_pool') + ->tags('tag_pool') + ->adapters(['cache.adapter.redis']) + ; - return static function (ContainerConfigurator $container) { - $container->extension('framework', [ - 'cache' => [ - 'pools' => [ - 'my_cache_pool' => [ - 'adapter' => 'cache.adapter.redis', - 'tags' => 'tag_pool', - ], - 'tag_pool' => [ - 'adapter' => 'cache.adapter.apcu', - ], - ], - ], - ]); + $framework->cache() + ->pool('tag_pool') + ->adapters(['cache.adapter.apcu']) + ; }; .. note:: @@ -757,10 +710,6 @@ To see all available cache pools: $ php bin/console cache:pool:list -.. versionadded:: 4.3 - - The ``cache:pool:list`` command was introduced in Symfony 4.3. - Clear one pool: .. code-block:: terminal @@ -778,3 +727,104 @@ Clear all caches everywhere: .. code-block:: terminal $ php bin/console cache:pool:clear cache.global_clearer + +Clear cache by tag(s): + +.. versionadded:: 6.1 + + The ``cache:pool:invalidate-tags`` command was introduced in Symfony 6.1. + +.. code-block:: terminal + + # invalidate tag1 from all taggable pools + $ php bin/console cache:pool:invalidate-tags tag1 + + # invalidate tag1 & tag2 from all taggable pools + $ php bin/console cache:pool:invalidate-tags tag1 tag2 + + # invalidate tag1 & tag2 from cache.app pool + $ php bin/console cache:pool:invalidate-tags tag1 tag2 --pool=cache.app + + # invalidate tag1 & tag2 from cache1 & cache2 pools + $ php bin/console cache:pool:invalidate-tags tag1 tag2 -p cache1 -p cache2 + +Encrypting the Cache +-------------------- + +To encrypt the cache using ``libsodium``, you can use the +:class:`Symfony\\Component\\Cache\\Marshaller\\SodiumMarshaller`. + +First, you need to generate a secure key and add it to your :doc:`secret +store ` as ``CACHE_DECRYPTION_KEY``: + +.. code-block:: terminal + + $ php -r 'echo base64_encode(sodium_crypto_box_keypair());' + +Then, register the ``SodiumMarshaller`` service using this key: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/cache.yaml + + # ... + services: + Symfony\Component\Cache\Marshaller\SodiumMarshaller: + decorates: cache.default_marshaller + arguments: + - ['%env(base64:CACHE_DECRYPTION_KEY)%'] + # use multiple keys in order to rotate them + #- ['%env(base64:CACHE_DECRYPTION_KEY)%', '%env(base64:OLD_CACHE_DECRYPTION_KEY)%'] + - '@Symfony\Component\Cache\Marshaller\SodiumMarshaller.inner' + + .. code-block:: xml + + + + + + + + + + + env(base64:CACHE_DECRYPTION_KEY) + + + + + + + + + .. code-block:: php + + // config/packages/cache.php + use Symfony\Component\Cache\Marshaller\SodiumMarshaller; + use Symfony\Component\DependencyInjection\ChildDefinition; + use Symfony\Component\DependencyInjection\Reference; + + // ... + $container->setDefinition(SodiumMarshaller::class, new ChildDefinition('cache.default_marshaller')) + ->addArgument(['env(base64:CACHE_DECRYPTION_KEY)']) + // use multiple keys in order to rotate them + //->addArgument(['env(base64:CACHE_DECRYPTION_KEY)', 'env(base64:OLD_CACHE_DECRYPTION_KEY)']) + ->addArgument(new Reference(SodiumMarshaller::class.'.inner')); + +.. caution:: + + This will encrypt the values of the cache items, but not the cache keys. Be + careful not to leak sensitive data in the keys. + +When configuring multiple keys, the first key will be used for reading and +writing, and the additional key(s) will only be used for reading. Once all +cache items encrypted with the old key have expired, you can completely remove +``OLD_CACHE_DECRYPTION_KEY``. diff --git a/components/asset.rst b/components/asset.rst index 5be1003ef15..076a759bd9c 100644 --- a/components/asset.rst +++ b/components/asset.rst @@ -51,6 +51,8 @@ Installation Usage ----- +.. _asset-packages: + Asset Packages ~~~~~~~~~~~~~~ @@ -165,6 +167,34 @@ In those cases, use the echo $package->getUrl('css/app.css'); // result: build/css/app.b916426ea1d10021f3f17ce8031f93c2.css +If you request an asset that is *not found* in the ``rev-manifest.json`` file, +the original - *unmodified* - asset path will be returned. The ``$strictMode`` +argument helps debug issues because it throws an exception when the asset is not +listed in the manifest:: + + use Symfony\Component\Asset\Package; + use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy; + + // The value of $strictMode can be specific per environment "true" for debugging and "false" for stability. + $strictMode = true; + // assumes the JSON file above is called "rev-manifest.json" + $package = new Package(new JsonManifestVersionStrategy(__DIR__.'/rev-manifest.json', null, $strictMode)); + + echo $package->getUrl('not-found.css'); + // error: + +If your JSON file is not on your local filesystem but is accessible over HTTP, +use the :class:`Symfony\\Component\\Asset\\VersionStrategy\\RemoteJsonManifestVersionStrategy` +with the :doc:`HttpClient component `:: + + use Symfony\Component\Asset\Package; + use Symfony\Component\Asset\VersionStrategy\RemoteJsonManifestVersionStrategy; + use Symfony\Component\HttpClient\HttpClient; + + $httpClient = HttpClient::create(); + $manifestUrl = 'https://cdn.example.com/rev-manifest.json'; + $package = new Package(new RemoteJsonManifestVersionStrategy($manifestUrl, $httpClient)); + Custom Version Strategies ......................... @@ -184,12 +214,12 @@ every day:: $this->version = date('Ymd'); } - public function getVersion($path) + public function getVersion(string $path) { return $this->version; } - public function applyVersion($path) + public function applyVersion(string $path) { return sprintf('%s?v=%s', $path, $this->getVersion($path)); } diff --git a/components/browser_kit.rst b/components/browser_kit.rst index 9a618d8bad2..b6cfa98d4a0 100644 --- a/components/browser_kit.rst +++ b/components/browser_kit.rst @@ -40,13 +40,7 @@ The component only provides an abstract client and does not provide any backend ready to use for the HTTP layer. To create your own client, you must extend the ``AbstractBrowser`` class and implement the :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::doRequest` method. - -.. deprecated:: 4.3 - - In Symfony 4.3 and earlier versions, the ``AbstractBrowser`` class was called - ``Client`` (which is now deprecated). - -The ``doRequest()`` method accepts a request and should return a response:: +This method accepts a request and should return a response:: namespace Acme; @@ -66,7 +60,7 @@ The ``doRequest()`` 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 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` +on ``HttpKernelInterface``, have a look at the :class:`Symfony\\Component\\HttpKernel\\HttpClientKernel` provided by the :doc:`HttpKernel component `. Making Requests @@ -86,6 +80,16 @@ 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\\AbstractBrowser::jsonRequest` method, +which defines the same arguments as the ``request()`` method, is a shortcut to +convert the request parameters into a JSON string and set the needed HTTP headers:: + + use Acme\Client; + + $client = new Client(); + // this encodes parameters as JSON and sets the required CONTENT_TYPE and HTTP_ACCEPT headers + $crawler = $client->jsonRequest('GET', '/', ['some_parameter' => 'some_value']); + The :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::xmlHttpRequest` method, which defines the same arguments as the ``request()`` method, is a shortcut to make AJAX requests:: @@ -167,6 +171,28 @@ provides access to the form properties (e.g. ``$form->getUri()``, // submit that form $crawler = $client->submit($form); +Custom Header Handling +~~~~~~~~~~~~~~~~~~~~~~ + +The optional HTTP headers passed to the ``request()`` method follow the FastCGI +request format (uppercase, underscores instead of dashes and prefixed with ``HTTP_``). +Before saving those headers to the request, they are lower-cased, with ``HTTP_`` +stripped, and underscores converted into dashes. + +If you're making a request to an application that has special rules about header +capitalization or punctuation, override the ``getHeaders()`` method, which must +return an associative array of headers:: + + protected function getHeaders(Request $request): array + { + $headers = parent::getHeaders($request); + if (isset($request->getServer()['api_key'])) { + $headers['api_key'] = $request->getServer()['api_key']; + } + + return $headers; + } + Cookies ------- @@ -323,10 +349,6 @@ dedicated web crawler or scraper such as `Goutte`_:: ``query``. They have to be passed as the default options argument to the client which is used by the HTTP browser. -.. versionadded:: 4.3 - - The feature to make external HTTP requests was introduced in Symfony 4.3. - Dealing with HTTP responses ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -339,6 +361,20 @@ the requests you made. To do so, call the ``getResponse()`` method of the $browser->request('GET', 'https://foo.com'); $response = $browser->getResponse(); +If you're making requests that result in a JSON response, you may use the +``toArray()`` method to turn the JSON document into a PHP array without having +to call ``json_decode()`` explicitly:: + + $browser = new HttpBrowser(HttpClient::create()); + + $browser->request('GET', 'https://api.foo.com'); + $response = $browser->getResponse()->toArray(); + // $response is a PHP array of the decoded JSON contents + +.. versionadded:: 6.1 + + The ``toArray()`` method was introduced in Symfony 6.1. + Learn more ---------- diff --git a/components/cache.rst b/components/cache.rst index 9a1b6611f8d..270081d2e3c 100644 --- a/components/cache.rst +++ b/components/cache.rst @@ -16,9 +16,8 @@ The Cache Component .. tip:: - The component also contains adapters to convert between PSR-6, PSR-16 and - Doctrine caches. See :doc:`/components/cache/psr6_psr16_adapters` and - :doc:`/components/cache/adapters/doctrine_adapter`. + The component also contains adapters to convert between PSR-6 and PSR-16. + See :doc:`/components/cache/psr6_psr16_adapters`. Installation ------------ @@ -159,7 +158,7 @@ concepts: **Adapter** It implements the actual caching mechanism to store the information in the filesystem, in a database, etc. The component provides several ready to use - adapters for common caching backends (Redis, APCu, Doctrine, PDO, etc.) + adapters for common caching backends (Redis, APCu, PDO, etc.) Basic Usage (PSR-6) ------------------- @@ -195,6 +194,34 @@ Now you can create, retrieve, update and delete items using this cache pool:: For a list of all of the supported adapters, see :doc:`/components/cache/cache_pools`. +Marshalling (Serializing) Data +------------------------------ + +.. note:: + + `Marshalling`_ and `serializing`_ are similar concepts. Serializing is the + process of translating an object state into a format that can be stored + (e.g. in a file). Marshalling is the process of translating both the object + state and its codebase into a format that can be stored or transmitted. + + Unmarshalling an object produces a copy of the original object, possibly by + automatically loading the class definitions of the object. + +Symfony uses *marshallers* (classes which implement +:class:`Symfony\\Component\\Cache\\Marshaller\\MarshallerInterface`) to process +the cache items before storing them. + +The :class:`Symfony\\Component\\Cache\\Marshaller\\DefaultMarshaller` uses PHP's +``serialize()`` or ``igbinary_serialize()`` if the `Igbinary extension`_ is installed. +There are other *marshallers* that can encrypt or compress the data before storing it:: + + use Symfony\Component\Cache\Adapter\RedisAdapter; + use Symfony\Component\Cache\DefaultMarshaller; + use Symfony\Component\Cache\DeflateMarshaller; + + $marshaller = new DeflateMarshaller(new DefaultMarshaller()); + $cache = new RedisAdapter(new \Redis(), 'namespace', 0, $marshaller); + Advanced Usage -------------- @@ -208,3 +235,6 @@ Advanced Usage .. _`Cache Contracts`: https://github.com/symfony/contracts/blob/master/Cache/CacheInterface.php .. _`Stampede prevention`: https://en.wikipedia.org/wiki/Cache_stampede .. _Probabilistic early expiration: https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration +.. _`Marshalling`: https://en.wikipedia.org/wiki/Marshalling_(computer_science) +.. _`serializing`: https://en.wikipedia.org/wiki/Serialization +.. _`Igbinary extension`: https://github.com/igbinary/igbinary diff --git a/components/cache/adapters/array_cache_adapter.rst b/components/cache/adapters/array_cache_adapter.rst index ffdc1d60dd0..baa7f840590 100644 --- a/components/cache/adapters/array_cache_adapter.rst +++ b/components/cache/adapters/array_cache_adapter.rst @@ -8,10 +8,7 @@ Array Cache Adapter Generally, this adapter is useful for testing purposes, as its contents are stored in memory and not persisted outside the running PHP process in any way. It can also be useful while warming up caches, due to the :method:`Symfony\\Component\\Cache\\Adapter\\ArrayAdapter::getValues` -method. - -This adapter can be passed a default cache lifetime as its first parameter, and a boolean that -toggles serialization as its second parameter:: +method:: use Symfony\Component\Cache\Adapter\ArrayAdapter; @@ -23,5 +20,13 @@ toggles serialization as its second parameter:: $defaultLifetime = 0, // if ``true``, the values saved in the cache are serialized before storing them - $storeSerialized = true + $storeSerialized = true, + + // the maximum lifetime (in seconds) of the entire cache (after this time, the + // entire cache is deleted to avoid stale data from consuming memory) + $maxLifetime = 0, + + // the maximum number of items that can be stored in the cache. When the limit + // is reached, cache follows the LRU model (least recently used items are deleted) + $maxItems = 0 ); diff --git a/components/cache/adapters/couchbasebucket_adapter.rst b/components/cache/adapters/couchbasebucket_adapter.rst new file mode 100644 index 00000000000..e5043978690 --- /dev/null +++ b/components/cache/adapters/couchbasebucket_adapter.rst @@ -0,0 +1,146 @@ +.. index:: + single: Cache Pool + single: Couchbase Cache + +.. _couchbase-adapter: + +Couchbase Bucket Cache Adapter +============================== + +This adapter stores the values in-memory using one (or more) `Couchbase server`_ +instances. Unlike the :ref:`APCu adapter `, and similarly to the +:ref:`Memcached adapter `, it is not limited to the current server's +shared memory; you can store contents independent of your PHP environment. +The ability to utilize a cluster of servers to provide redundancy and/or fail-over +is also available. + +.. caution:: + + **Requirements:** The `Couchbase PHP extension`_ as well as a `Couchbase server`_ + must be installed, active, and running to use this adapter. Version ``2.6`` or + less than 3.0 of the `Couchbase PHP extension`_ is required for this adapter. + +This adapter expects a `Couchbase Bucket`_ instance to be passed as the first +parameter. A namespace and default cache lifetime can optionally be passed as +the second and third parameters:: + + use Symfony\Component\Cache\Adapter\CouchbaseBucketAdapter; + + $cache = new CouchbaseBucketAdapter( + // the client object that sets options and adds the server instance(s) + $client, + + // the name of bucket + $bucket, + + // a string prefixed to the keys of the items stored in this cache + $namespace, + + // the default lifetime (in seconds) for cache items that do not define their + // own lifetime, with a value 0 causing items to be stored indefinitely + $defaultLifetime + ); + + +Configure the Connection +------------------------ + +The :method:`Symfony\\Component\\Cache\\Adapter\\CouchbaseBucketAdapter::createConnection` +helper method allows creating and configuring a `Couchbase Bucket`_ class instance using a +`Data Source Name (DSN)`_ or an array of DSNs:: + + use Symfony\Component\Cache\Adapter\CouchbaseBucketAdapter; + + // pass a single DSN string to register a single server with the client + $client = CouchbaseBucketAdapter::createConnection( + 'couchbase://localhost' + // the DSN can include config options (pass them as a query string): + // 'couchbase://localhost:11210?operationTimeout=10' + // 'couchbase://localhost:11210?operationTimeout=10&configTimeout=20' + ); + + // pass an array of DSN strings to register multiple servers with the client + $client = CouchbaseBucketAdapter::createConnection([ + 'couchbase://10.0.0.100', + 'couchbase://10.0.0.101', + 'couchbase://10.0.0.102', + // etc... + ]); + + // a single DSN can define multiple servers using the following syntax: + // host[hostname-or-IP:port] (where port is optional). Sockets must include a trailing ':' + $client = CouchbaseBucketAdapter::createConnection( + 'couchbase:?host[localhost]&host[localhost:12345]' + ); + + +Configure the Options +--------------------- + +The :method:`Symfony\\Component\\Cache\\Adapter\\CouchbaseBucketAdapter::createConnection` +helper method also accepts an array of options as its second argument. The +expected format is an associative array of ``key => value`` pairs representing +option names and their respective values:: + + use Symfony\Component\Cache\Adapter\CouchbaseBucketAdapter; + + $client = CouchbaseBucketAdapter::createConnection( + // a DSN string or an array of DSN strings + [], + + // associative array of configuration options + [ + 'username' => 'xxxxxx', + 'password' => 'yyyyyy', + 'configTimeout' => '100', + ] + ); + +Available Options +~~~~~~~~~~~~~~~~~ + +``username`` (type: ``string``) + Username for connection ``CouchbaseCluster``. + +``password`` (type: ``string``) + Password of connection ``CouchbaseCluster``. + +``operationTimeout`` (type: ``int``, default: ``2500000``) + The operation timeout (in microseconds) is the maximum amount of time the library will + wait for an operation to receive a response before invoking its callback with a failure status. + +``configTimeout`` (type: ``int``, default: ``5000000``) + How long (in microseconds) the client will wait to obtain the initial configuration. + +``configNodeTimeout`` (type: ``int``, default: ``2000000``) + Per-node configuration timeout (in microseconds). + +``viewTimeout`` (type: ``int``, default: ``75000000``) + The I/O timeout (in microseconds) for HTTP requests to Couchbase Views API. + +``httpTimeout`` (type: ``int``, default: ``75000000``) + The I/O timeout (in microseconds) for HTTP queries (management API). + +``configDelay`` (type: ``int``, default: ``10000``) + Config refresh throttling + Modify the amount of time (in microseconds) before the configuration error threshold will forcefully be set to its maximum number forcing a configuration refresh. + +``htconfigIdleTimeout`` (type: ``int``, default: ``4294967295``) + Idling/Persistence for HTTP bootstrap (in microseconds). + +``durabilityInterval`` (type: ``int``, default: ``100000``) + The time (in microseconds) the client will wait between repeated probes to a given server. + +``durabilityTimeout`` (type: ``int``, default: ``5000000``) + The time (in microseconds) the client will spend sending repeated probes to a given key's vBucket masters and replicas before they are deemed not to have satisfied the durability requirements. + +.. tip:: + + Reference the `Couchbase Bucket`_ extension's `predefined constants`_ documentation + for additional information about the available options. + +.. _`Couchbase PHP extension`: https://docs.couchbase.com/sdk-api/couchbase-php-client-2.6.0/files/couchbase.html +.. _`predefined constants`: https://docs.couchbase.com/sdk-api/couchbase-php-client-2.6.0/classes/Couchbase.Bucket.html +.. _`Couchbase server`: https://couchbase.com/ +.. _`Couchbase Bucket`: https://docs.couchbase.com/sdk-api/couchbase-php-client-2.6.0/classes/Couchbase.Bucket.html +.. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name diff --git a/components/cache/adapters/couchbasecollection_adapter.rst b/components/cache/adapters/couchbasecollection_adapter.rst new file mode 100644 index 00000000000..f00e54a6e2b --- /dev/null +++ b/components/cache/adapters/couchbasecollection_adapter.rst @@ -0,0 +1,143 @@ +.. index:: + single: Cache Pool + single: Couchabase Cache + +.. _couchbase-collection-adapter: + +Couchbase Collection Cache Adapter +================================== + +This adapter stores the values in-memory using one (or more) `Couchbase server`_ +instances. Unlike the :ref:`APCu adapter `, and similarly to the +:ref:`Memcached adapter `, it is not limited to the current server's +shared memory; you can store contents independent of your PHP environment. +The ability to utilize a cluster of servers to provide redundancy and/or fail-over +is also available. + +.. caution:: + + **Requirements:** The `Couchbase PHP extension`_ as well as a `Couchbase server`_ + must be installed, active, and running to use this adapter. Version ``3.0`` or + greater of the `Couchbase PHP extension`_ is required for this adapter. + +This adapter expects a `Couchbase Collection`_ instance to be passed as the first +parameter. A namespace and default cache lifetime can optionally be passed as +the second and third parameters:: + + use Symfony\Component\Cache\Adapter\CouchbaseCollectionAdapter; + + $cache = new CouchbaseCollectionAdapter( + // the client object that sets options and adds the server instance(s) + $client, + + // a string prefixed to the keys of the items stored in this cache + $namespace, + + // the default lifetime (in seconds) for cache items that do not define their + // own lifetime, with a value 0 causing items to be stored indefinitely + $defaultLifetime + ); + + +Configure the Connection +------------------------ + +The :method:`Symfony\\Component\\Cache\\Adapter\\CouchbaseCollectionAdapter::createConnection` +helper method allows creating and configuring a `Couchbase Collection`_ class instance using a +`Data Source Name (DSN)`_ or an array of DSNs:: + + use Symfony\Component\Cache\Adapter\CouchbaseCollectionAdapter; + + // pass a single DSN string to register a single server with the client + $client = CouchbaseCollectionAdapter::createConnection( + 'couchbase://localhost' + // the DSN can include config options (pass them as a query string): + // 'couchbase://localhost:11210?operationTimeout=10' + // 'couchbase://localhost:11210?operationTimeout=10&configTimout=20' + ); + + // pass an array of DSN strings to register multiple servers with the client + $client = CouchbaseCollectionAdapter::createConnection([ + 'couchbase://10.0.0.100', + 'couchbase://10.0.0.101', + 'couchbase://10.0.0.102', + // etc... + ]); + + // a single DSN can define multiple servers using the following syntax: + // host[hostname-or-IP:port] (where port is optional). Sockets must include a trailing ':' + $client = CouchbaseCollectionAdapter::createConnection( + 'couchbase:?host[localhost]&host[localhost:12345]' + ); + + +Configure the Options +--------------------- + +The :method:`Symfony\\Component\\Cache\\Adapter\\CouchbaseCollectionAdapter::createConnection` +helper method also accepts an array of options as its second argument. The +expected format is an associative array of ``key => value`` pairs representing +option names and their respective values:: + + use Symfony\Component\Cache\Adapter\CouchbaseCollectionAdapter; + + $client = CouchbaseCollectionAdapter::createConnection( + // a DSN string or an array of DSN strings + [], + + // associative array of configuration options + [ + 'username' => 'xxxxxx', + 'password' => 'yyyyyy', + 'configTimeout' => '100', + ] + ); + +Available Options +~~~~~~~~~~~~~~~~~ + +``username`` (type: ``string``) + Username for connection ``CouchbaseCluster``. + +``password`` (type: ``string``) + Password of connection ``CouchbaseCluster``. + +``operationTimeout`` (type: ``int``, default: ``2500000``) + The operation timeout (in microseconds) is the maximum amount of time the library will + wait for an operation to receive a response before invoking its callback with a failure status. + +``configTimeout`` (type: ``int``, default: ``5000000``) + How long (in microseconds) the client will wait to obtain the initial configuration. + +``configNodeTimeout`` (type: ``int``, default: ``2000000``) + Per-node configuration timeout (in microseconds). + +``viewTimeout`` (type: ``int``, default: ``75000000``) + The I/O timeout (in microseconds) for HTTP requests to Couchbase Views API. + +``httpTimeout`` (type: ``int``, default: ``75000000``) + The I/O timeout (in microseconds) for HTTP queries (management API). + +``configDelay`` (type: ``int``, default: ``10000``) + Config refresh throttling + Modify the amount of time (in microseconds) before the configuration error threshold will forcefully be set to its maximum number forcing a configuration refresh. + +``htconfigIdleTimeout`` (type: ``int``, default: ``4294967295``) + Idling/Persistence for HTTP bootstrap (in microseconds). + +``durabilityInterval`` (type: ``int``, default: ``100000``) + The time (in microseconds) the client will wait between repeated probes to a given server. + +``durabilityTimeout`` (type: ``int``, default: ``5000000``) + The time (in microseconds) the client will spend sending repeated probes to a given key's vBucket masters and replicas before they are deemed not to have satisfied the durability requirements. + +.. tip:: + + Reference the `Couchbase Collection`_ extension's `predefined constants`_ documentation + for additional information about the available options. + +.. _`Couchbase PHP extension`: https://docs.couchbase.com/sdk-api/couchbase-php-client/namespaces/couchbase.html +.. _`predefined constants`: https://docs.couchbase.com/sdk-api/couchbase-php-client/classes/Couchbase-Bucket.html +.. _`Couchbase server`: https://couchbase.com/ +.. _`Couchbase Collection`: https://docs.couchbase.com/sdk-api/couchbase-php-client/classes/Couchbase-Collection.html +.. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name diff --git a/components/cache/adapters/doctrine_adapter.rst b/components/cache/adapters/doctrine_adapter.rst deleted file mode 100644 index 198ae19338c..00000000000 --- a/components/cache/adapters/doctrine_adapter.rst +++ /dev/null @@ -1,42 +0,0 @@ -.. index:: - single: Cache Pool - single: Doctrine Cache - -.. _doctrine-adapter: - -Doctrine Cache Adapter -====================== - -This adapter wraps any class extending the `Doctrine Cache`_ abstract provider, allowing -you to use these providers in your application as if they were Symfony Cache adapters. - -This adapter expects a ``\Doctrine\Common\Cache\CacheProvider`` instance as its first -parameter, and optionally a namespace and default cache lifetime as its second and -third parameters:: - - use Doctrine\Common\Cache\CacheProvider; - use Doctrine\Common\Cache\SQLite3Cache; - use Symfony\Component\Cache\Adapter\DoctrineAdapter; - - $provider = new SQLite3Cache(new \SQLite3(__DIR__.'/cache/data.sqlite'), 'youTableName'); - - $cache = new DoctrineAdapter( - - // a cache provider instance - CacheProvider $provider, - - // a string prefixed to the keys of the items stored in this cache - $namespace = '', - - // the default lifetime (in seconds) for cache items that do not define their - // own lifetime, with a value 0 causing items to be stored indefinitely (i.e. - // until the database table is truncated or its rows are otherwise deleted) - $defaultLifetime = 0 - ); - -.. tip:: - - A :class:`Symfony\\Component\\Cache\\DoctrineProvider` class is also provided by the - component to use any PSR6-compatible implementations with Doctrine-compatible classes. - -.. _`Doctrine Cache`: https://github.com/doctrine/cache diff --git a/components/cache/adapters/memcached_adapter.rst b/components/cache/adapters/memcached_adapter.rst index 1b8103433e1..009ead59cbd 100644 --- a/components/cache/adapters/memcached_adapter.rst +++ b/components/cache/adapters/memcached_adapter.rst @@ -70,10 +70,6 @@ helper method allows creating and configuring a `Memcached`_ class instance usin 'memcached:?host[localhost]&host[localhost:12345]&host[/some/memcached.sock:]=3' ); -.. versionadded:: 4.2 - - The option to define multiple servers in a single DSN was introduced in Symfony 4.2. - The `Data Source Name (DSN)`_ for this adapter must use the following format: .. code-block:: text diff --git a/components/cache/adapters/pdo_doctrine_dbal_adapter.rst b/components/cache/adapters/pdo_doctrine_dbal_adapter.rst index b840da76de7..9239f276f6a 100644 --- a/components/cache/adapters/pdo_doctrine_dbal_adapter.rst +++ b/components/cache/adapters/pdo_doctrine_dbal_adapter.rst @@ -7,16 +7,26 @@ PDO & Doctrine DBAL Cache Adapter ================================= -This adapter stores the cache items in an SQL database. It requires a :phpclass:`PDO`, -`Doctrine DBAL Connection`_, or `Data Source Name (DSN)`_ as its first parameter, and -optionally a namespace, default cache lifetime, and options array as its second, -third, and forth parameters:: +The PDO and Doctrine DBAL adapters store the cache items in a table of an SQL database. + +.. note:: + + These adapters implement :class:`Symfony\\Component\\Cache\\PruneableInterface`, + allowing for manual :ref:`pruning of expired cache entries ` + by calling the ``prune()`` method. + +Using PHP PDO +------------- + +The :class:`Symfony\\Component\\Cache\\Adapter\\PdoAdapter` requires a :phpclass:`PDO`, +or `Data Source Name (DSN)`_ as its first parameter. You can pass a namespace, +default cache lifetime, and options array as the other optional arguments:: use Symfony\Component\Cache\Adapter\PdoAdapter; $cache = new PdoAdapter( - // a PDO, a Doctrine DBAL connection or DSN for lazy connecting through PDO + // a PDO connection or DSN for lazy connecting through PDO $databaseConnectionOrDSN, // the string prefixed to the keys of the items stored in this cache @@ -40,13 +50,43 @@ your code. .. tip:: When passed a `Data Source Name (DSN)`_ string (instead of a database connection - class instance), the connection will be lazy-loaded when needed. + class instance), the connection will be lazy-loaded when needed. DBAL Connection + are lazy-loaded by default; some additional options may be necessary to detect + the database engine and version without opening the connection. + +Using Doctrine DBAL +------------------- + +The :class:`Symfony\\Component\\Cache\\Adapter\\DoctrineDbalAdapter` requires a +`Doctrine DBAL Connection`_, or `Doctrine DBAL URL`_ as its first parameter. +You can pass a namespace, default cache lifetime, and options array as the other +optional arguments:: + + use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter; + + $cache = new DoctrineDbalAdapter( + + // a Doctrine DBAL connection or DBAL URL + $databaseConnectionOrURL, + + // the string prefixed to the keys of the items stored in this cache + $namespace = '', + + // the default lifetime (in seconds) for cache items that do not define their + // own lifetime, with a value 0 causing items to be stored indefinitely (i.e. + // until the database table is truncated or its rows are otherwise deleted) + $defaultLifetime = 0, + + // an array of options for configuring the database table and connection + $options = [] + ); .. note:: - This adapter implements :class:`Symfony\\Component\\Cache\\PruneableInterface`, - allowing for manual :ref:`pruning of expired cache entries ` by - calling its ``prune()`` method. + DBAL Connection are lazy-loaded by default; some additional options may be + necessary to detect the database engine and version without opening the + connection. .. _`Doctrine DBAL Connection`: https://github.com/doctrine/dbal/blob/master/src/Connection.php +.. _`Doctrine DBAL URL`: https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url .. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name diff --git a/components/cache/adapters/redis_adapter.rst b/components/cache/adapters/redis_adapter.rst index 842e717c874..9596386b80e 100644 --- a/components/cache/adapters/redis_adapter.rst +++ b/components/cache/adapters/redis_adapter.rst @@ -124,14 +124,6 @@ parameter to set the name of your service group:: 'redis:default:verysecurepassword@?host[redis1:26379]&host[redis2:26379]&host[redis3:26379]&redis_sentinel=mymaster&dbindex=3' ); -.. versionadded:: 4.2 - - The option to define multiple servers in a single DSN was introduced in Symfony 4.2. - -.. versionadded:: 4.4 - - Redis Sentinel support was introduced in Symfony 4.4. - .. note:: See the :class:`Symfony\\Component\\Cache\\Traits\\RedisTrait` for more options diff --git a/components/cache/psr6_psr16_adapters.rst b/components/cache/psr6_psr16_adapters.rst index 28a41fca0e7..6b98d26744b 100644 --- a/components/cache/psr6_psr16_adapters.rst +++ b/components/cache/psr6_psr16_adapters.rst @@ -46,10 +46,6 @@ this use-case:: // now use this wherever you want $githubApiClient = new GitHubApiClient($psr6Cache); -.. versionadded:: 4.3 - - The ``Psr16Adapter`` class was introduced in Symfony 4.3. - Using a PSR-6 Cache Object as a PSR-16 Cache -------------------------------------------- @@ -87,8 +83,4 @@ this use-case:: // now use this wherever you want $githubApiClient = new GitHubApiClient($psr16Cache); -.. versionadded:: 4.3 - - The ``Psr16Cache`` class was introduced in Symfony 4.3. - .. _`PSR-16`: https://www.php-fig.org/psr/psr-16/ diff --git a/components/config/definition.rst b/components/config/definition.rst index 2864bddb570..8ad8ae1b0c9 100644 --- a/components/config/definition.rst +++ b/components/config/definition.rst @@ -68,10 +68,6 @@ implements the :class:`Symfony\\Component\\Config\\Definition\\ConfigurationInte } } -.. deprecated:: 4.2 - - Not passing the root node name to ``TreeBuilder`` was deprecated in Symfony 4.2. - Adding Node Definitions to the Tree ----------------------------------- @@ -447,11 +443,15 @@ method:: ->children() ->integerNode('old_option') // this outputs the following generic deprecation message: - // The child node "old_option" at path "..." is deprecated. - ->setDeprecated() + // Since acme/package 1.2: The child node "old_option" at path "..." is deprecated. + ->setDeprecated('acme/package', '1.2') // you can also pass a custom deprecation message (%node% and %path% placeholders are available): - ->setDeprecated('The "%node%" option is deprecated. Use "new_config_option" instead.') + ->setDeprecated( + 'acme/package', + '1.2', + 'The "%node%" option is deprecated. Use "new_config_option" instead.' + ) ->end() ->end() ; diff --git a/components/console/changing_default_command.rst b/components/console/changing_default_command.rst index 6eb9f2b5227..6a2fe877478 100644 --- a/components/console/changing_default_command.rst +++ b/components/console/changing_default_command.rst @@ -10,14 +10,14 @@ name to the ``setDefaultCommand()`` method:: namespace Acme\Console\Command; + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; + #[AsCommand(name: 'hello:world')] class HelloWorldCommand extends Command { - protected static $defaultName = 'hello:world'; - protected function configure() { $this->setDescription('Outputs "Hello World"'); diff --git a/components/console/console_arguments.rst b/components/console/console_arguments.rst index 79f5c6c1f4c..5b641c26774 100644 --- a/components/console/console_arguments.rst +++ b/components/console/console_arguments.rst @@ -14,6 +14,7 @@ Have a look at the following command that has three options:: namespace Acme\Console\Command; + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; @@ -21,14 +22,12 @@ Have a look at the following command that has three options:: use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; + #[AsCommand(name: 'demo:args', description: 'Describe args behaviors')] class DemoArgsCommand extends Command { - protected static $defaultName = 'demo:args'; - protected function configure() { $this - ->setDescription('Describe args behaviors') ->setDefinition( new InputDefinition([ new InputOption('foo', 'f'), diff --git a/components/console/events.rst b/components/console/events.rst index 7183c2e75f7..f41c50d3bec 100644 --- a/components/console/events.rst +++ b/components/console/events.rst @@ -154,4 +154,70 @@ Listeners receive a It is then dispatched just after the ``ConsoleEvents::ERROR`` event. The exit code received in this case is the exception code. +The ``ConsoleEvents::SIGNAL`` Event +----------------------------------- + +**Typical Purposes**: To perform some actions after the command execution was interrupted. + +`Signals`_ are asynchronous notifications sent to a process in order to notify +it of an event that occurred. For example, when you press ``Ctrl + C`` in a +command, the operating system sends the ``SIGINT`` signal to it. + +When a command is interrupted, Symfony dispatches the ``ConsoleEvents::SIGNAL`` +event. Listen to this event so you can perform some actions (e.g. logging some +results, cleaning some temporary files, etc.) before finishing the command execution. + +Listeners receive a +:class:`Symfony\\Component\\Console\\Event\\ConsoleSignalEvent` event:: + + use Symfony\Component\Console\ConsoleEvents; + use Symfony\Component\Console\Event\ConsoleSignalEvent; + + $dispatcher->addListener(ConsoleEvents::SIGNAL, function (ConsoleSignalEvent $event) { + + // gets the signal number + $signal = $event->getHandlingSignal(); + + if (\SIGINT === $signal) { + echo "bye bye!"; + } + }); + +.. tip:: + + All the available signals (``SIGINT``, ``SIGQUIT``, etc.) are defined as + `constants of the PCNTL PHP extension`_. + +If you use the Console component inside a Symfony application, commands can +handle signals themselves. To do so, implement the +:class:`Symfony\\Component\\Console\\Command\\SignalableCommandInterface` and subscribe to one or more signals:: + + // src/Command/SomeCommand.php + namespace App\Command; + + use Symfony\Component\Console\Command\Command; + use Symfony\Component\Console\Command\SignalableCommandInterface; + + class SomeCommand extends Command implements SignalableCommandInterface + { + // ... + + public function getSubscribedSignals(): array + { + // return here any of the constants defined by PCNTL extension + return [\SIGINT, \SIGTERM]; + } + + public function handleSignal(int $signal): void + { + if (\SIGINT === $signal) { + // ... + } + + // ... + } + } + .. _`reserved exit codes`: https://www.tldp.org/LDP/abs/html/exitcodes.html +.. _`Signals`: https://en.wikipedia.org/wiki/Signal_(IPC) +.. _`constants of the PCNTL PHP extension`: https://www.php.net/manual/en/pcntl.constants.php diff --git a/components/console/helpers/cursor.rst b/components/console/helpers/cursor.rst new file mode 100644 index 00000000000..c7a6556a9cb --- /dev/null +++ b/components/console/helpers/cursor.rst @@ -0,0 +1,99 @@ +.. index:: + single: Console Helpers; Cursor Helper + +Cursor Helper +============= + +The :class:`Symfony\\Component\\Console\\Cursor` allows you to change the +cursor position in a console command. This allows you to write on any position +of the output: + +.. image:: /_images/components/console/cursor.gif + :align: center + +.. code-block:: php + + // src/Command/MyCommand.php + namespace App\Command; + + use Symfony\Component\Console\Command\Command; + use Symfony\Component\Console\Cursor; + use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Output\OutputInterface; + + class MyCommand extends Command + { + // ... + + public function execute(InputInterface $input, OutputInterface $output): int + { + // ... + + $cursor = new Cursor($output); + + // moves the cursor to a specific column (1st argument) and + // row (2nd argument) position + $cursor->moveToPosition(7, 11); + + // and write text on this position using the output + $output->write('My text'); + + // ... + } + } + +Using the cursor +---------------- + +Moving the cursor +................. + +There are few methods to control moving the command cursor:: + + // moves the cursor 1 line up from its current position + $cursor->moveUp(); + + // moves the cursor 3 lines up from its current position + $cursor->moveUp(3); + + // same for down + $cursor->moveDown(); + + // moves the cursor 1 column right from its current position + $cursor->moveRight(); + + // moves the cursor 3 columns right from its current position + $cursor->moveRight(3); + + // same for left + $cursor->moveLeft(); + + // move the cursor to a specific (column, row) position from the + // top-left position of the terminal + $cursor->moveToPosition(7, 11); + +You can get the current command's cursor position by using:: + + $position = $cursor->getCurrentPosition(); + // $position[0] // columns (aka x coordinate) + // $position[1] // rows (aka y coordinate) + +Clearing output +............... + +The cursor can also clear some output on the screen:: + + // clears all the output from the current line + $cursor->clearLine(); + + // clears all the output from the current line after the current position + $cursor->clearLineAfter(); + + // clears all the output from the cursors' current position to the end of the screen + $cursor->clearOutput(); + + // clears the entire screen + $cursor->clearScreen(); + +You also can leverage the :method:`Symfony\\Component\\Console\\Cursor::show` +and :method:`Symfony\\Component\\Console\\Cursor::hide` methods on the cursor. diff --git a/components/console/helpers/debug_formatter.rst b/components/console/helpers/debug_formatter.rst index 89609da8419..e824fac89a2 100644 --- a/components/console/helpers/debug_formatter.rst +++ b/components/console/helpers/debug_formatter.rst @@ -7,8 +7,8 @@ Debug Formatter Helper The :class:`Symfony\\Component\\Console\\Helper\\DebugFormatterHelper` provides functions to output debug information when running an external program, for instance a process or HTTP request. For example, if you used it to output -the results of running ``ls -la`` on a UNIX system, it might output something -like this: +the results of running ``figlet symfony``, it might output something like +this: .. image:: /_images/components/console/debug_formatter.png :align: center diff --git a/components/console/helpers/index.rst b/components/console/helpers/index.rst index 5f328d47472..09546769655 100644 --- a/components/console/helpers/index.rst +++ b/components/console/helpers/index.rst @@ -13,6 +13,7 @@ The Console Helpers questionhelper table debug_formatter + cursor The Console component comes with some useful helpers. These helpers contain functions to ease some common tasks. diff --git a/components/console/helpers/map.rst.inc b/components/console/helpers/map.rst.inc index 68e1e722a87..8f9ce0ca0f3 100644 --- a/components/console/helpers/map.rst.inc +++ b/components/console/helpers/map.rst.inc @@ -4,3 +4,4 @@ * :doc:`/components/console/helpers/questionhelper` * :doc:`/components/console/helpers/table` * :doc:`/components/console/helpers/debug_formatter` +* :doc:`/components/console/helpers/cursor` diff --git a/components/console/helpers/progressbar.rst b/components/console/helpers/progressbar.rst index 2e2de44e399..94f2a550f80 100644 --- a/components/console/helpers/progressbar.rst +++ b/components/console/helpers/progressbar.rst @@ -46,6 +46,22 @@ Instead of advancing the bar by a number of steps (with the you can also set the current progress by calling the :method:`Symfony\\Component\\Console\\Helper\\ProgressBar::setProgress` method. +If you are resuming long-standing tasks, it's useful to start drawing the progress +bar at a certain point. Use the second optional argument of ``start()`` to set +that starting point:: + + use Symfony\Component\Console\Helper\ProgressBar; + + // creates a new progress bar (100 units) + $progressBar = new ProgressBar($output, 100); + + // displays the progress bar starting at 25 completed units + $progressBar->start(null, 25); + +.. versionadded:: 6.2 + + The option to start a progress bar at a certain point was introduced in Symfony 6.2. + .. tip:: If your platform doesn't support ANSI codes, updates to the progress @@ -56,11 +72,6 @@ you can also set the current progress by calling the to redraw every N iterations. By default, redraw frequency is **100ms** or **10%** of your ``max``. - .. versionadded:: 4.4 - - The ``minSecondsBetweenRedraws()`` and ``maxSecondsBetweenRedraws()`` - methods were introduced in Symfony 4.4. - If you don't know the exact number of steps in advance, set it to a reasonable value and then call the ``setMaxSteps()`` method to update it as needed:: @@ -130,10 +141,6 @@ The previous code will output: 1/2 [==============>-------------] 50% 2/2 [============================] 100% -.. versionadded:: 4.3 - - The ``iterate()`` method was introduced in Symfony 4.3. - Customizing the Progress Bar ---------------------------- @@ -302,7 +309,7 @@ to display it can be customized:: .. caution:: For performance reasons, Symfony redraws the screen once every 100ms. If this is too - fast or to slow for your application, use the methods + fast or too slow for your application, use the methods :method:`Symfony\\Component\\Console\\Helper\\ProgressBar::minSecondsBetweenRedraws` and :method:`Symfony\\Component\\Console\\Helper\\ProgressBar::maxSecondsBetweenRedraws`:: @@ -322,11 +329,6 @@ to display it can be customized:: $progressBar->advance(); } - .. versionadded:: 4.4 - - The ``minSecondsBetweenRedraws`` and ``maxSecondsBetweenRedraws()`` methods - were introduced in Symfony 4.4. - Custom Placeholders ~~~~~~~~~~~~~~~~~~~ @@ -363,7 +365,7 @@ placeholder before displaying the progress bar:: // 0/100 -- Start $progressBar->setMessage('Task is in progress...'); - $progressBar->advance(); + $progressBar->advance(); // 1/100 -- Task is in progress... Messages can be combined with custom placeholders too. In this example, the diff --git a/components/console/helpers/questionhelper.rst b/components/console/helpers/questionhelper.rst index e736e288fb4..edc70c03a1e 100644 --- a/components/console/helpers/questionhelper.rst +++ b/components/console/helpers/questionhelper.rst @@ -34,14 +34,18 @@ the following to your command:: { // ... - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { $helper = $this->getHelper('question'); $question = new ConfirmationQuestion('Continue with this action?', false); if (!$helper->ask($input, $output, $question)) { - return 0; + return Command::SUCCESS; } + + // ... do something here + + return Command::SUCCESS; } } @@ -75,12 +79,16 @@ if you want to know a bundle name, you can add this to your command:: use Symfony\Component\Console\Question\Question; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { // ... $question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle'); $bundleName = $helper->ask($input, $output, $question); + + // ... do something with the bundleName + + return Command::SUCCESS; } The user will be asked "Please enter the name of the bundle". They can type @@ -99,12 +107,13 @@ from a predefined list:: use Symfony\Component\Console\Question\ChoiceQuestion; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { // ... $helper = $this->getHelper('question'); $question = new ChoiceQuestion( 'Please select your favorite color (defaults to red)', + // choices can also be PHP objects that implement __toString() method ['red', 'blue', 'yellow'], 0 ); @@ -114,6 +123,8 @@ from a predefined list:: $output->writeln('You have just selected: '.$color); // ... do something with the color + + return Command::SUCCESS; } The option which should be selected by default is provided with the third @@ -137,7 +148,7 @@ this use :method:`Symfony\\Component\\Console\\Question\\ChoiceQuestion::setMult use Symfony\Component\Console\Question\ChoiceQuestion; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { // ... $helper = $this->getHelper('question'); @@ -150,6 +161,8 @@ this use :method:`Symfony\\Component\\Console\\Question\\ChoiceQuestion::setMult $colors = $helper->ask($input, $output, $question); $output->writeln('You have just selected: ' . implode(', ', $colors)); + + return Command::SUCCESS; } Now, when the user enters ``1,2``, the result will be: @@ -167,7 +180,7 @@ will be autocompleted as the user types:: use Symfony\Component\Console\Question\Question; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { // ... $helper = $this->getHelper('question'); @@ -177,6 +190,10 @@ will be autocompleted as the user types:: $question->setAutocompleterValues($bundles); $bundleName = $helper->ask($input, $output, $question); + + // ... do something with the bundleName + + return Command::SUCCESS; } In more complex use cases, it may be necessary to generate suggestions on the @@ -186,7 +203,7 @@ provide a callback function to dynamically generate suggestions:: use Symfony\Component\Console\Question\Question; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { $helper = $this->getHelper('question'); @@ -212,12 +229,12 @@ provide a callback function to dynamically generate suggestions:: $question->setAutocompleterCallback($callback); $filePath = $helper->ask($input, $output, $question); + + // ... do something with the filePath + + return Command::SUCCESS; } -.. versionadded:: 4.3 - - The ``setAutocompleterCallback()`` method was introduced in Symfony 4.3. - Do not Trim the Answer ~~~~~~~~~~~~~~~~~~~~~~ @@ -227,7 +244,7 @@ You can also specify if you want to not trim the answer by setting it directly w use Symfony\Component\Console\Question\Question; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { // ... $helper = $this->getHelper('question'); @@ -236,11 +253,40 @@ You can also specify if you want to not trim the answer by setting it directly w $question->setTrimmable(false); // if the users inputs 'elsa ' it will not be trimmed and you will get 'elsa ' as value $name = $helper->ask($input, $output, $question); + + // ... do something with the name + + return Command::SUCCESS; } -.. versionadded:: 4.4 +Accept Multiline Answers +~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the question helper stops reading user input when it receives a newline +character (i.e., when the user hits ``ENTER`` once). However, you may specify that +the response to a question should allow multiline answers by passing ``true`` to +:method:`Symfony\\Component\\Console\\Question\\Question::setMultiline`:: + + use Symfony\Component\Console\Question\Question; + + // ... + public function execute(InputInterface $input, OutputInterface $output): int + { + // ... + $helper = $this->getHelper('question'); + + $question = new Question('How do you solve world peace?'); + $question->setMultiline(true); + + $answer = $helper->ask($input, $output, $question); + + // ... do something with the answer + + return Command::SUCCESS; + } - The ``setTrimmable()`` method was introduced in Symfony 4.4. +Multiline questions stop reading user input after receiving an end-of-transmission +control character (``Ctrl-D`` on Unix systems or ``Ctrl-Z`` on Windows). Hiding the User's Response ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -251,7 +297,7 @@ convenient for passwords:: use Symfony\Component\Console\Question\Question; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { // ... $helper = $this->getHelper('question'); @@ -261,6 +307,10 @@ convenient for passwords:: $question->setHiddenFallback(false); $password = $helper->ask($input, $output, $question); + + // ... do something with the password + + return Command::SUCCESS; } .. caution:: @@ -284,13 +334,15 @@ convenient for passwords:: use Symfony\Component\Console\Question\ChoiceQuestion; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { // ... $helper = $this->getHelper('question'); QuestionHelper::disableStty(); // ... + + return Command::SUCCESS; } Normalizing the Answer @@ -306,7 +358,7 @@ method:: use Symfony\Component\Console\Question\Question; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { // ... $helper = $this->getHelper('question'); @@ -318,6 +370,10 @@ method:: }); $bundleName = $helper->ask($input, $output, $question); + + // ... do something with the bundleName + + return Command::SUCCESS; } .. caution:: @@ -326,6 +382,8 @@ method:: of the validator. If the answer is invalid, don't throw exceptions in the normalizer and let the validator handle those errors. +.. _console-validate-question-answer: + Validating the Answer --------------------- @@ -338,7 +396,7 @@ method:: use Symfony\Component\Console\Question\Question; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { // ... $helper = $this->getHelper('question'); @@ -356,6 +414,10 @@ method:: $question->setMaxAttempts(2); $bundleName = $helper->ask($input, $output, $question); + + // ... do something with the bundleName + + return Command::SUCCESS; } The ``$validator`` is a callback which handles the validation. It should @@ -370,6 +432,22 @@ If you reach this max number it will use the default value. Using ``null`` means the number of attempts is infinite. The user will be asked as long as they provide an invalid answer and will only be able to proceed if their input is valid. +.. tip:: + + You can even use the :doc:`Validator ` component to + validate the input by using the :method:`Symfony\\Component\\Validator\\Validation::createCallable` + method:: + + use Symfony\Component\Validator\Constraints\Regex; + use Symfony\Component\Validator\Validation; + + $question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle'); + $validation = Validation::createCallable(new Regex([ + 'pattern' => '/^[a-zA-Z]+Bundle$/', + 'message' => 'The name of the bundle should be suffixed with \'Bundle\'', + ])); + $question->setValidator($validation); + Validating a Hidden Response ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -378,7 +456,7 @@ You can also use a validator with a hidden question:: use Symfony\Component\Console\Question\Question; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { // ... $helper = $this->getHelper('question'); @@ -398,6 +476,10 @@ You can also use a validator with a hidden question:: $question->setMaxAttempts(20); $password = $helper->ask($input, $output, $question); + + // ... do something with the password + + return Command::SUCCESS; } Testing a Command that Expects Input @@ -406,8 +488,6 @@ Testing a Command that Expects Input If you want to write a unit test for a command which expects some kind of input from the command line, you need to set the inputs that the command expects:: - use Symfony\Component\Console\Helper\HelperSet; - use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Tester\CommandTester; // ... diff --git a/components/console/helpers/table.rst b/components/console/helpers/table.rst index 9b479d0f9a6..df9797a6be8 100644 --- a/components/console/helpers/table.rst +++ b/components/console/helpers/table.rst @@ -28,7 +28,7 @@ set the headers, set the rows and then render the table:: class SomeCommand extends Command { - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { $table = new Table($output); $table @@ -41,6 +41,8 @@ set the headers, set the rows and then render the table:: ]) ; $table->render(); + + return Command::SUCCESS; } } @@ -150,6 +152,31 @@ The output of this command will be: | (the rest of the rows...) | +-------+------------+--------------------------------+ +By default, table contents are displayed horizontally. You can change this behavior +via the :method:`Symfony\\Component\\Console\\Helper\\Table::setVertical` method:: + + // ... + $table->setVertical(); + $table->render(); + +The output of this command will be: + +.. code-block:: terminal + + +------------------------------+ + | ISBN: 99921-58-10-7 | + | Title: Divine Comedy | + | Author: Dante Alighieri | + |------------------------------| + | ISBN: 9971-5-0210-0 | + | Title: A Tale of Two Cities | + | Author: Charles Dickens | + +------------------------------+ + +.. versionadded:: 6.1 + + Support for vertical rendering was introduced in Symfony 6.1. + The table style can be changed to any built-in styles via :method:`Symfony\\Component\\Console\\Helper\\Table::setStyle`:: @@ -265,6 +292,35 @@ Here is a full list of things you can customize: This method can also be used to override a built-in style. +In addition to the built-in table styles, you can also apply different styles +to each table cell via :class:`Symfony\\Component\\Console\\Helper\\TableCellStyle`:: + + use Symfony\Component\Console\Helper\Table; + use Symfony\Component\Console\Helper\TableCellStyle; + + $table = new Table($output); + + $table->setRows([ + [ + '978-0804169127', + new TableCell( + 'Divine Comedy', + [ + 'style' => new TableCellStyle([ + 'align' => 'center', + 'fg' => 'red', + 'bg' => 'green', + + // or + 'cellFormat' => '%s', + ]) + ] + ) + ], + ]); + + $table->render(); + Spanning Multiple Columns and Rows ---------------------------------- @@ -373,7 +429,7 @@ The only requirement to append rows is that the table must be rendered inside a class SomeCommand extends Command { - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { $section = $output->section(); $table = new Table($section); @@ -382,6 +438,8 @@ The only requirement to append rows is that the table must be rendered inside a $table->render(); $table->appendRow(['Symfony']); + + return Command::SUCCESS; } } diff --git a/components/console/logger.rst b/components/console/logger.rst index 8f029e47002..25fce56d7d9 100644 --- a/components/console/logger.rst +++ b/components/console/logger.rst @@ -37,24 +37,18 @@ You can rely on the logger to use this dependency inside a command:: namespace Acme\Console\Command; use Acme\MyDependency; + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\OutputInterface; + #[AsCommand( + name: 'my:command', + description: 'Use an external dependency requiring a PSR-3 logger' + )] class MyCommand extends Command { - protected static $defaultName = 'my:command'; - - protected function configure() - { - $this - ->setDescription( - 'Use an external dependency requiring a PSR-3 logger' - ) - ; - } - protected function execute(InputInterface $input, OutputInterface $output) { $logger = new ConsoleLogger($output); diff --git a/components/console/single_command_tool.rst b/components/console/single_command_tool.rst index ff4c2be8f1c..08a51cd943d 100644 --- a/components/console/single_command_tool.rst +++ b/components/console/single_command_tool.rst @@ -12,27 +12,22 @@ it is possible to remove this need by declaring a single command application:: register('echo') - ->addArgument('foo', InputArgument::OPTIONAL, 'The directory') - ->addOption('bar', null, InputOption::VALUE_REQUIRED) - ->setCode(function(InputInterface $input, OutputInterface $output) { - // output arguments and options - }) - ->getApplication() - ->setDefaultCommand('echo', true) // Single command application + use Symfony\Component\Console\SingleCommandApplication; + + (new SingleCommandApplication()) + ->setName('My Super Command') // Optional + ->setVersion('1.0.0') // Optional + ->addArgument('foo', InputArgument::OPTIONAL, 'The directory') + ->addOption('bar', null, InputOption::VALUE_REQUIRED) + ->setCode(function (InputInterface $input, OutputInterface $output) { + // output arguments and options + }) ->run(); -The :method:`Symfony\\Component\\Console\\Application::setDefaultCommand` method -accepts a boolean as the second parameter. If true, the command ``echo`` will then -always be used, without having to pass its name. - You can still register a command as usual:: #!/usr/bin/env php @@ -49,3 +44,7 @@ You can still register a command as usual:: $application->setDefaultCommand($command->getName(), true); $application->run(); + +The :method:`Symfony\\Component\\Console\\Application::setDefaultCommand` method +accepts a boolean as second parameter. If true, the command ``echo`` will then +always be used, without having to pass its name. diff --git a/components/contracts.rst b/components/contracts.rst index 1f1cc3f6adc..a1ae32192f6 100644 --- a/components/contracts.rst +++ b/components/contracts.rst @@ -61,7 +61,7 @@ convention. For example: { "...": "...", "provide": { - "symfony/cache-implementation": "1.0" + "symfony/cache-implementation": "3.0" } } diff --git a/components/css_selector.rst b/components/css_selector.rst index 0d3dbf8dbf9..649a34293a4 100644 --- a/components/css_selector.rst +++ b/components/css_selector.rst @@ -98,10 +98,6 @@ Pseudo-classes are partially supported: ``li:first-of-type``) but not with the ``*`` selector). * Supported: ``*:only-of-type``. -.. versionadded:: 4.4 - - The support for ``*:only-of-type`` was introduced in Symfony 4.4. - Learn more ---------- diff --git a/components/dependency_injection.rst b/components/dependency_injection.rst index fab46ff3d26..2c3cce60476 100644 --- a/components/dependency_injection.rst +++ b/components/dependency_injection.rst @@ -297,12 +297,16 @@ config files: ->set('mailer.transport', 'sendmail') ; - $container->services() - ->set('mailer', 'Mailer') - ->args(['%mailer.transport%']) + $services = $container->services(); + $services->set('mailer', 'Mailer') + ->args(['%mailer.transport%']) - ->set('newsletter_manager', 'NewsletterManager') - ->call('setMailer', [ref('mailer')]) + $services->set('mailer', 'Mailer') + ->args([param('mailer.transport')]) + ; + + $services->set('newsletter_manager', 'NewsletterManager') + ->call('setMailer', [service('mailer')]) ; }; diff --git a/components/dependency_injection/compilation.rst b/components/dependency_injection/compilation.rst index 3f5812529b2..4d8fb3e54e3 100644 --- a/components/dependency_injection/compilation.rst +++ b/components/dependency_injection/compilation.rst @@ -471,6 +471,14 @@ serves at dumping the compiled container:: file_put_contents($file, $dumper->dump()); } +.. tip:: + + The ``file_put_contents()`` function is not atomic. That could cause issues + in a production environment with multiple concurrent requests. Instead, use + the :ref:`dumpFile() method ` from Symfony Filesystem + component or other methods provided by Symfony (e.g. ``$containerConfigCache->write()``) + which are atomic. + ``ProjectServiceContainer`` is the default name given to the dumped container class. However, you can change this with the ``class`` option when you dump it:: diff --git a/components/dom_crawler.rst b/components/dom_crawler.rst index fd35b5e7fd8..02ddadff58c 100644 --- a/components/dom_crawler.rst +++ b/components/dom_crawler.rst @@ -77,10 +77,6 @@ tree. The DomCrawler component will use it automatically when the content has an HTML5 doctype. - .. versionadded:: 4.3 - - The automatic support of the html5-php library was introduced in Symfony 4.3. - Node Filtering ~~~~~~~~~~~~~~ @@ -170,10 +166,6 @@ Verify if the current node matches a selector:: $crawler->matches('p.lorem'); -.. versionadded:: 4.4 - - The ``matches()`` method was introduced in Symfony 4.4. - Node Traversing ~~~~~~~~~~~~~~~ @@ -195,10 +187,10 @@ Get the same level nodes after or before the current selection:: $crawler->filter('body > p')->nextAll(); $crawler->filter('body > p')->previousAll(); -Get all the child or parent nodes:: +Get all the child or ancestor nodes:: $crawler->filter('body')->children(); - $crawler->filter('body > p')->parents(); + $crawler->filter('body > p')->ancestors(); Get all the direct child nodes matching a CSS selector:: @@ -208,10 +200,6 @@ Get the first parent (heading toward the document root) of the element that matc $crawler->closest('p.lorem'); -.. versionadded:: 4.4 - - The ``closest()`` method was introduced in Symfony 4.4. - .. note:: All the traversal methods return a new :class:`Symfony\\Component\\DomCrawler\\Crawler` @@ -233,17 +221,16 @@ Access the value of the first node of the current selection:: // avoid the exception passing an argument that text() returns when node does not exist $message = $crawler->filterXPath('//body/p')->text('Default text content'); - // pass TRUE as the second argument of text() to remove all extra white spaces, including - // the internal ones (e.g. " foo\n bar baz \n " is returned as "foo bar baz") - $crawler->filterXPath('//body/p')->text('Default text content', true); + // by default, text() trims white spaces, including the internal ones + // (e.g. " foo\n bar baz \n " is returned as "foo bar baz") + // pass FALSE as the second argument to return the original text unchanged + $crawler->filterXPath('//body/p')->text('Default text content', false); -.. versionadded:: 4.3 - - The default argument of ``text()`` was introduced in Symfony 4.3. - -.. versionadded:: 4.4 - - The option to trim white spaces in ``text()`` was introduced in Symfony 4.4. + // innerText() is similar to text() but only returns the text that is + // the direct descendant of the current node, excluding any child nodes + $text = $crawler->filterXPath('//body/p')->innerText(); + // if content is

Foo Bar

+ // innerText() returns 'Foo' and text() returns 'Foo Bar' Access the attribute value of the first node of the current selection:: @@ -261,10 +248,6 @@ Extract attribute and/or node values from the list of nodes:: Special attribute ``_text`` represents a node value, while ``_name`` represents the element name (the HTML tag name). - .. versionadded:: 4.3 - - The special attribute ``_name`` was introduced in Symfony 4.3. - Call an anonymous function on each node of the list:: use Symfony\Component\DomCrawler\Crawler; @@ -360,19 +343,11 @@ and :phpclass:`DOMNode` objects:: // avoid the exception passing an argument that html() returns when node does not exist $html = $crawler->html('Default HTML content'); - .. versionadded:: 4.3 - - The default argument of ``html()`` was introduced in Symfony 4.3. - Or you can get the outer HTML of the first node using :method:`Symfony\\Component\\DomCrawler\\Crawler::outerHtml`:: $html = $crawler->outerHtml(); - .. versionadded:: 4.4 - - The ``outerHtml()`` method was introduced in Symfony 4.4. - Expression Evaluation ~~~~~~~~~~~~~~~~~~~~~ @@ -524,10 +499,6 @@ useful methods for working with forms:: $method = $form->getMethod(); $name = $form->getName(); -.. versionadded:: 4.4 - - The ``getName()`` method was introduced in Symfony 4.4. - The :method:`Symfony\\Component\\DomCrawler\\Form::getUri` method does more than just return the ``action`` attribute of the form. If the form method is GET, then it mimics the browser's behavior and returns the ``action`` @@ -661,6 +632,19 @@ the whole form or specific field(s):: $form->disableValidation(); $form['country']->select('Invalid value'); +Resolving a URI +~~~~~~~~~~~~~~~ + +The :class:`Symfony\\Component\\DomCrawler\\UriResolver` class takes an URI +(relative, absolute, fragment, etc.) and turns it into an absolute URI against +another given base URI:: + + use Symfony\Component\DomCrawler\UriResolver; + + UriResolver::resolve('/foo', 'http://localhost/bar/foo/'); // http://localhost/foo + UriResolver::resolve('?a=b', 'http://localhost/bar#foo'); // http://localhost/bar?a=b + UriResolver::resolve('../../', 'http://localhost/'); // http://localhost/ + Learn more ---------- diff --git a/components/event_dispatcher.rst b/components/event_dispatcher.rst index dd1ce9c310a..45955506e5c 100644 --- a/components/event_dispatcher.rst +++ b/components/event_dispatcher.rst @@ -254,21 +254,11 @@ determine which instance is passed. Note that ``AddEventAliasesPass`` has to be processed before ``RegisterListenersPass``. - By default, the listeners pass assumes that the event dispatcher's service + The listeners pass assumes that the event dispatcher's service id is ``event_dispatcher``, that event listeners are tagged with the ``kernel.event_listener`` tag, that event subscribers are tagged with the ``kernel.event_subscriber`` tag and that the alias mapping is - stored as parameter ``event_dispatcher.event_aliases``. You can change these - default values by passing custom values to the constructors of - ``RegisterListenersPass`` and ``AddEventAliasesPass``. - -.. versionadded:: 4.3 - - Aliasing event names is possible since Symfony 4.3. - -.. versionadded:: 4.4 - - The ``AddEventAliasesPass`` class was introduced in Symfony 4.4. + stored as parameter ``event_dispatcher.event_aliases``. .. _event_dispatcher-closures-as-listeners: diff --git a/components/event_dispatcher/traceable_dispatcher.rst b/components/event_dispatcher/traceable_dispatcher.rst index 87d58023445..33a98a2336b 100644 --- a/components/event_dispatcher/traceable_dispatcher.rst +++ b/components/event_dispatcher/traceable_dispatcher.rst @@ -41,10 +41,10 @@ to register event listeners and dispatch events:: $traceableEventDispatcher->dispatch($event, 'event.the_name'); After your application has been processed, you can use the -:method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcherInterface::getCalledListeners` +:method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher::getCalledListeners` method to retrieve an array of event listeners that have been called in your application. Similarly, the -:method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcherInterface::getNotCalledListeners` +:method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher::getNotCalledListeners` method returns an array of event listeners that have not been called:: // ... diff --git a/components/expression_language/syntax.rst b/components/expression_language/syntax.rst index b78ac907ca8..de0fddf80bf 100644 --- a/components/expression_language/syntax.rst +++ b/components/expression_language/syntax.rst @@ -14,16 +14,20 @@ Supported Literals The component supports: * **strings** - single and double quotes (e.g. ``'hello'``) -* **numbers** - e.g. ``103`` +* **numbers** - integers (e.g. ``103``), decimals (e.g. ``9.95``), decimals + without leading zeros (e.g. ``.99``, equivalent to ``0.99``); all numbers + support optional underscores as separators to improve readability (e.g. + ``1_000_000``, ``3.14159_26535``) * **arrays** - using JSON-like notation (e.g. ``[1, 2]``) * **hashes** - using JSON-like notation (e.g. ``{ foo: 'bar' }``) * **booleans** - ``true`` and ``false`` * **null** - ``null`` * **exponential** - also known as scientific (e.g. ``1.99E+3`` or ``1e-2``) - .. versionadded:: 4.4 - - The ``exponential`` literal was introduced in Symfony 4.4. +.. versionadded:: 6.1 + + Support for decimals without leading zeros and underscore separators were + introduced in Symfony 6.1. .. caution:: @@ -98,6 +102,25 @@ JavaScript:: This will print out ``Hi Hi Hi!``. +Null-safe Operator +~~~~~~~~~~~~~~~~~~ + +Use the ``?.`` syntax to access properties and methods of objects that can be +``null`` (this is equivalent to the ``$object?->propertyOrMethod`` PHP null-safe +operator):: + + // these will throw an exception when `fruit` is `null` + $expressionLanguage->evaluate('fruit.color', ['fruit' => '...']) + $expressionLanguage->evaluate('fruit.getStock()', ['fruit' => '...']) + + // these will return `null` if `fruit` is `null` + $expressionLanguage->evaluate('fruit?.color', ['fruit' => '...']) + $expressionLanguage->evaluate('fruit?.getStock()', ['fruit' => '...']) + +.. versionadded:: 6.1 + + The null safe operator was introduced in Symfony 6.1. + .. _component-expression-functions: Working with Functions @@ -187,6 +210,14 @@ Comparison Operators * ``<=`` (less than or equal to) * ``>=`` (greater than or equal to) * ``matches`` (regex match) +* ``contains`` +* ``starts with`` +* ``ends with`` + +.. versionadded:: 6.1 + + The ``contains``, ``starts with`` and ``ends with`` operators were introduced + in Symfony 6.1. .. tip:: @@ -312,6 +343,23 @@ Ternary Operators * ``foo ?: 'no'`` (equal to ``foo ? foo : 'no'``) * ``foo ? 'yes'`` (equal to ``foo ? 'yes' : ''``) +Null Coalescing Operator +~~~~~~~~~~~~~~~~~~~~~~~~ + +This is the same as the PHP `null-coalescing operator`_, which combines +the ternary operator and ``isset()``. It returns the left hand-side if it exists +and it's not ``null``; otherwise it returns the right hand-side. Note that you +can chain multiple coalescing operators. + +* ``foo ?? 'no'`` +* ``foo.baz ?? 'no'`` +* ``foo[3] ?? 'no'`` +* ``foo.baz ?? foo['baz'] ?? 'no'`` + +.. versionadded:: 6.2 + + The null-coalescing operator was introduced in Symfony 6.2. + Built-in Objects and Variables ------------------------------ @@ -322,3 +370,5 @@ expressions (e.g. the request, the current user, etc.): * :doc:`Variables available in security expressions `; * :doc:`Variables available in service container expressions `; * :ref:`Variables available in routing expressions `. + +.. _`null-coalescing operator`: https://www.php.net/manual/en/language.operators.comparison.php#language.operators.comparison.coalesce diff --git a/components/filesystem.rst b/components/filesystem.rst index a56ed09da0b..67e6c745c14 100644 --- a/components/filesystem.rst +++ b/components/filesystem.rst @@ -4,7 +4,8 @@ The Filesystem Component ======================== - The Filesystem component provides basic utilities for the filesystem. + The Filesystem component provides platform-independent utilities for + filesystem operations and for file/directory paths manipulation. Installation ------------ @@ -18,20 +19,26 @@ Installation Usage ----- -The :class:`Symfony\\Component\\Filesystem\\Filesystem` class is the unique -endpoint for filesystem operations:: +The component contains two main classes called :class:`Symfony\\Component\\Filesystem\\Filesystem` +and :class:`Symfony\\Component\\Filesystem\\Path`:: use Symfony\Component\Filesystem\Exception\IOExceptionInterface; use Symfony\Component\Filesystem\Filesystem; + use Symfony\Component\Filesystem\Path; $filesystem = new Filesystem(); try { - $filesystem->mkdir(sys_get_temp_dir().'/'.random_int(0, 1000)); + $filesystem->mkdir( + Path::normalize(sys_get_temp_dir().'/'.random_int(0, 1000)), + ); } catch (IOExceptionInterface $exception) { echo "An error occurred while creating your directory at ".$exception->getPath(); } +Filesystem Utilities +-------------------- + ``mkdir`` ~~~~~~~~~ @@ -212,15 +219,22 @@ systems (unlike PHP's :phpfunction:`readlink` function):: // returns its absolute fully resolved final version of the target (if there are nested links, they are resolved) $filesystem->readlink('/path/to/link', true); -Its behavior is the following:: +Its behavior is the following: + +* When ``$canonicalize`` is ``false``: -* When ``$canonicalize`` is ``false`` (the default value): - * if ``$path`` does not exist or is not a link, it returns ``null``. - * if ``$path`` is a link, it returns the next direct target of the link without considering the existence of the target. + * if ``$path`` does not exist or is not a link, it returns ``null``. + * if ``$path`` is a link, it returns the next direct target of the link without considering the existence of the target. * When ``$canonicalize`` is ``true``: - * if ``$path`` does not exist, it returns null. - * if ``$path`` exists, it returns its absolute fully resolved final version. + + * if ``$path`` does not exist, it returns null. + * if ``$path`` exists, it returns its absolute fully resolved final version. + +.. note:: + + If you wish to canonicalize the path without checking its existence, you can + use :method:`Symfony\\Component\\Filesystem\\Path::canonicalize` method instead. ``makePathRelative`` ~~~~~~~~~~~~~~~~~~~~ @@ -270,15 +284,20 @@ exception on failure:: // returns a path like : /tmp/prefix_wyjgtF $filesystem->tempnam('/tmp', 'prefix_'); + // returns a path like : /tmp/prefix_wyjgtF.png + $filesystem->tempnam('/tmp', 'prefix_', '.png'); + +.. _filesystem-dumpfile: ``dumpFile`` ~~~~~~~~~~~~ :method:`Symfony\\Component\\Filesystem\\Filesystem::dumpFile` saves the given -contents into a file. It does this in an atomic manner: it writes a temporary -file first and then moves it to the new file location when it's finished. -This means that the user will always see either the complete old file or -complete new file (but never a partially-written file):: +contents into a file (creating the file and its directory if they don't exist). +It does this in an atomic manner: it writes a temporary file first and then moves +it to the new file location when it's finished. This means that the user will +always see either the complete old file or complete new file (but never a +partially-written file):: $filesystem->dumpFile('file.txt', 'Hello World'); @@ -291,10 +310,190 @@ The ``file.txt`` file contains ``Hello World`` now. contents at the end of some file:: $filesystem->appendToFile('logs.txt', 'Email sent to user@example.com'); + // the third argument tells whether the file should be locked when writing to it + $filesystem->appendToFile('logs.txt', 'Email sent to user@example.com', true); If either the file or its containing directory doesn't exist, this method creates them before appending the contents. +Path Manipulation Utilities +--------------------------- + +Dealing with file paths usually involves some difficulties: + +- Platform differences: file paths look different on different platforms. UNIX + file paths start with a slash ("/"), while Windows file paths start with a + system drive ("C:"). UNIX uses forward slashes, while Windows uses backslashes + by default. +- Absolute/relative paths: web applications frequently need to deal with absolute + and relative paths. Converting one to the other properly is tricky and repetitive. + +:class:`Symfony\\Component\\Filesystem\\Path` provides utility methods to tackle +those issues. + +Canonicalization +~~~~~~~~~~~~~~~~ + +Returns the shortest path name equivalent to the given path. It applies the +following rules iteratively until no further processing can be done: + +- "." segments are removed; +- ".." segments are resolved; +- backslashes ("\\") are converted into forward slashes ("/"); +- root paths ("/" and "C:/") always terminate with a slash; +- non-root paths never terminate with a slash; +- schemes (such as "phar://") are kept; +- replace "~" with the user's home directory. + +You can canonicalize a path with :method:`Symfony\\Component\\Filesystem\\Path::canonicalize`:: + + echo Path::canonicalize('/var/www/vhost/webmozart/../config.ini'); + // => /var/www/vhost/config.ini + +You can pass absolute paths and relative paths to the +:method:`Symfony\\Component\\Filesystem\\Path::canonicalize` method. When a +relative path is passed, ".." segments at the beginning of the path are kept:: + + echo Path::canonicalize('../uploads/../config/config.yaml'); + // => ../config/config.yaml + +Malformed paths are returned unchanged:: + + echo Path::canonicalize('C:Programs/PHP/php.ini'); + // => C:Programs/PHP/php.ini + +Converting Absolute/Relative Paths +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Absolute/relative paths can be converted with the methods +:method:`Symfony\\Component\\Filesystem\\Path::makeAbsolute` +and :method:`Symfony\\Component\\Filesystem\\Path::makeRelative`. + +:method:`Symfony\\Component\\Filesystem\\Path::makeAbsolute` method expects a +relative path and a base path to base that relative path upon:: + + echo Path::makeAbsolute('config/config.yaml', '/var/www/project'); + // => /var/www/project/config/config.yaml + +If an absolute path is passed in the first argument, the absolute path is +returned unchanged:: + + echo Path::makeAbsolute('/usr/share/lib/config.ini', '/var/www/project'); + // => /usr/share/lib/config.ini + +The method resolves ".." segments, if there are any:: + + echo Path::makeAbsolute('../config/config.yaml', '/var/www/project/uploads'); + // => /var/www/project/config/config.yaml + +This method is very useful if you want to be able to accept relative paths (for +example, relative to the root directory of your project) and absolute paths at +the same time. + +:method:`Symfony\\Component\\Filesystem\\Path::makeRelative` is the inverse +operation to :method:`Symfony\\Component\\Filesystem\\Path::makeAbsolute`:: + + echo Path::makeRelative('/var/www/project/config/config.yaml', '/var/www/project'); + // => config/config.yaml + +If the path is not within the base path, the method will prepend ".." segments +as necessary:: + + echo Path::makeRelative('/var/www/project/config/config.yaml', '/var/www/project/uploads'); + // => ../config/config.yaml + +Use :method:`Symfony\\Component\\Filesystem\\Path::isAbsolute` and +:method:`Symfony\\Component\\Filesystem\\Path::isRelative` to check whether a +path is absolute or relative:: + + Path::isAbsolute('C:\Programs\PHP\php.ini') + // => true + +All four methods internally canonicalize the passed path. + +Finding Longest Common Base Paths +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you store absolute file paths on the file system, this leads to a lot of +duplicated information:: + + return [ + '/var/www/vhosts/project/httpdocs/config/config.yaml', + '/var/www/vhosts/project/httpdocs/config/routing.yaml', + '/var/www/vhosts/project/httpdocs/config/services.yaml', + '/var/www/vhosts/project/httpdocs/images/banana.gif', + '/var/www/vhosts/project/httpdocs/uploads/images/nicer-banana.gif', + ]; + +Especially when storing many paths, the amount of duplicated information is +noticeable. You can use :method:`Symfony\\Component\\Filesystem\\Path::getLongestCommonBasePath` +to check a list of paths for a common base path:: + + Path::getLongestCommonBasePath( + '/var/www/vhosts/project/httpdocs/config/config.yaml', + '/var/www/vhosts/project/httpdocs/config/routing.yaml', + '/var/www/vhosts/project/httpdocs/config/services.yaml', + '/var/www/vhosts/project/httpdocs/images/banana.gif', + '/var/www/vhosts/project/httpdocs/uploads/images/nicer-banana.gif' + ); + // => /var/www/vhosts/project/httpdocs + +Use this path together with :method:`Symfony\\Component\\Filesystem\\Path::makeRelative` +to shorten the stored paths:: + + $bp = '/var/www/vhosts/project/httpdocs'; + + return [ + $bp.'/config/config.yaml', + $bp.'/config/routing.yaml', + $bp.'/config/services.yaml', + $bp.'/images/banana.gif', + $bp.'/uploads/images/nicer-banana.gif', + ]; + +:method:`Symfony\\Component\\Filesystem\\Path::getLongestCommonBasePath` always +returns canonical paths. + +Use :method:`Symfony\\Component\\Filesystem\\Path::isBasePath` to test whether a +path is a base path of another path:: + + Path::isBasePath("/var/www", "/var/www/project"); + // => true + + Path::isBasePath("/var/www", "/var/www/project/.."); + // => true + + Path::isBasePath("/var/www", "/var/www/project/../.."); + // => false + +Finding Directories/Root Directories +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PHP offers the function :phpfunction:`dirname` to obtain the directory path of a +file path. This method has a few quirks:: + +- `dirname()` does not accept backslashes on UNIX +- `dirname("C:/Programs")` returns "C:", not "C:/" +- `dirname("C:/")` returns ".", not "C:/" +- `dirname("C:")` returns ".", not "C:/" +- `dirname("Programs")` returns ".", not "" +- `dirname()` does not canonicalize the result + +:method:`Symfony\\Component\\Filesystem\\Path::getDirectory` fixes these +shortcomings:: + + echo Path::getDirectory("C:\Programs"); + // => C:/ + +Additionally, you can use :method:`Symfony\\Component\\Filesystem\\Path::getRoot` +to obtain the root of a path:: + + echo Path::getRoot("/etc/apache2/sites-available"); + // => / + + echo Path::getRoot("C:\Programs\Apache\Config"); + // => C:/ + Error Handling -------------- diff --git a/components/finder.rst b/components/finder.rst index 0279b967b0a..7246c5ebafd 100644 --- a/components/finder.rst +++ b/components/finder.rst @@ -141,16 +141,21 @@ default when looking for files and directories, but you can change this with the $finder->ignoreVCS(false); -If the search directory contains a ``.gitignore`` file, you can reuse those -rules to exclude files and directories from the results with the +If the search directory and its subdirectories contain ``.gitignore`` files, you +can reuse those rules to exclude files and directories from the results with the :method:`Symfony\\Component\\Finder\\Finder::ignoreVCSIgnored` method:: // excludes files/directories matching the .gitignore patterns $finder->ignoreVCSIgnored(true); -.. versionadded:: 4.3 +The rules of a directory always override the rules of its parent directories. - The ``ignoreVCSIgnored()`` method was introduced in Symfony 4.3. +.. note:: + + Git looks for ``.gitignore`` files starting from the repository root directory. + Symfony's Finder behavior is different and it looks for ``.gitignore`` files + starting from the directory used to search files/directories. To be consistent + with Git behavior, you should explicitly search from the Git repository root. File Name ~~~~~~~~~ @@ -248,11 +253,6 @@ Multiple paths can be excluded by chaining calls or passing an array:: // same as above $finder->notPath(['first/dir', 'other/dir']); -.. versionadded:: 4.2 - - Support for passing arrays to ``notPath()`` was introduced in Symfony - 4.2 - File Size ~~~~~~~~~ @@ -336,18 +336,30 @@ instance. The file is excluded from the result set if the Closure returns Sorting Results --------------- -Sort the results by name or by type (directories first, then files):: +Sort the results by name, extension, size or type (directories first, then files):: $finder->sortByName(); - + $finder->sortByCaseInsensitiveName(); + $finder->sortByExtension(); + $finder->sortBySize(); $finder->sortByType(); +.. versionadded:: 6.2 + + The ``sortByCaseInsensitiveName()``, ``sortByExtension()`` and ``sortBySize()`` + methods were introduced in Symfony 6.2. + .. tip:: By default, the ``sortByName()`` method uses the :phpfunction:`strcmp` PHP function (e.g. ``file1.txt``, ``file10.txt``, ``file2.txt``). Pass ``true`` as its argument to use PHP's `natural sort order`_ algorithm instead (e.g. ``file1.txt``, ``file2.txt``, ``file10.txt``). + + The ``sortByCaseInsensitiveName()`` method uses the case insensitive + :phpfunction:`strcasecmp` PHP function. Pass ``true`` as its argument to use + PHP's case insensitive `natural sort order`_ algorithm instead (i.e. the + :phpfunction:`strnatcasecmp` PHP function) Sort the files and directories by the last accessed, changed or modified time:: diff --git a/components/form.rst b/components/form.rst index ee13dd35289..d4fdab0a7ec 100644 --- a/components/form.rst +++ b/components/form.rst @@ -121,16 +121,17 @@ The following snippet adds CSRF protection to the form factory:: use Symfony\Component\Form\Extension\Csrf\CsrfExtension; use Symfony\Component\Form\Forms; - use Symfony\Component\HttpFoundation\Session\Session; + use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Csrf\CsrfTokenManager; use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator; use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage; - // creates a Session object from the HttpFoundation component - $session = new Session(); + // creates a RequestStack object using the current request + $requestStack = new RequestStack(); + $requestStack->push($request); $csrfGenerator = new UriSafeTokenGenerator(); - $csrfStorage = new SessionTokenStorage($session); + $csrfStorage = new SessionTokenStorage($requestStack); $csrfManager = new CsrfTokenManager($csrfGenerator, $csrfStorage); $formFactory = Forms::createFormFactoryBuilder() @@ -643,6 +644,12 @@ method: } } +.. caution:: + + The form's ``createView()`` method should be called *after* ``handleRequest()`` is + called. Otherwise, when using :doc:`form events `, changes done + in the ``*_SUBMIT`` events won't be applied to the view (like validation errors). + This defines a common form "workflow", which contains 3 different possibilities: #. On the initial GET request (i.e. when the user "surfs" to your page), @@ -775,4 +782,4 @@ Learn more /form/* .. _Twig: https://twig.symfony.com -.. _`Twig Configuration`: https://twig.symfony.com/doc/2.x/intro.html +.. _`Twig Configuration`: https://twig.symfony.com/doc/3.x/intro.html diff --git a/components/http_foundation.rst b/components/http_foundation.rst index 4ebce4526f0..a9131112d1d 100644 --- a/components/http_foundation.rst +++ b/components/http_foundation.rst @@ -81,19 +81,21 @@ can be accessed via several public properties: (``$request->headers->get('User-Agent')``). Each property is a :class:`Symfony\\Component\\HttpFoundation\\ParameterBag` -instance (or a sub-class of), which is a data holder class: +instance (or a subclass of), which is a data holder class: -* ``request``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag`; +* ``request``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag` or + :class:`Symfony\\Component\\HttpFoundation\\InputBag` if the data is + coming from ``$_POST`` parameters; -* ``query``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag`; +* ``query``: :class:`Symfony\\Component\\HttpFoundation\\InputBag`; -* ``cookies``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag`; +* ``cookies``: :class:`Symfony\\Component\\HttpFoundation\\InputBag`; * ``attributes``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag`; -* ``files``: :class:`Symfony\\Component\\HttpFoundation\\FileBag`; +* ``files``: :class:`Symfony\\Component\\HttpFoundation\\FileBag`; -* ``server``: :class:`Symfony\\Component\\HttpFoundation\\ServerBag`; +* ``server``: :class:`Symfony\\Component\\HttpFoundation\\ServerBag`; * ``headers``: :class:`Symfony\\Component\\HttpFoundation\\HeaderBag`. @@ -161,18 +163,23 @@ exist:: // returns 'baz' When PHP imports the request query, it handles request parameters like -``foo[bar]=baz`` in a special way as it creates an array. So you can get the -``foo`` parameter and you will get back an array with a ``bar`` element:: +``foo[bar]=baz`` in a special way as it creates an array. The ``get()`` method +doesn't support returning arrays, so you need to use the following code:: // the query string is '?foo[bar]=baz' - $request->query->get('foo'); + // don't use $request->query->get('foo'); use the following instead: + $request->query->all('foo'); // returns ['bar' => 'baz'] + // if the requested parameter does not exist, an empty array is returned: + $request->query->all('qux'); + // returns [] + $request->query->get('foo[bar]'); // returns null - $request->query->get('foo')['bar']; + $request->query->all()['foo']['bar']; // returns 'baz' .. _component-foundation-attributes: @@ -188,9 +195,14 @@ Finally, the raw data sent with the request body can be accessed using $content = $request->getContent(); -For instance, this may be useful to process a JSON string sent to the +For instance, this may be useful to process an XML string sent to the application by a remote service using the HTTP POST method. +If the request body is a JSON string, it can be accessed using +:method:`Symfony\\Component\\HttpFoundation\\Request::toArray`:: + + $data = $request->toArray(); + Identifying a Request ~~~~~~~~~~~~~~~~~~~~~ @@ -236,9 +248,9 @@ Accessing the Session ~~~~~~~~~~~~~~~~~~~~~ If you have a session attached to the request, you can access it via the -:method:`Symfony\\Component\\HttpFoundation\\Request::getSession` method; -the -:method:`Symfony\\Component\\HttpFoundation\\Request::hasPreviousSession` +``getSession()`` method of the :class:`Symfony\\Component\\HttpFoundation\\Request` +or :class:`Symfony\\Component\\HttpFoundation\\RequestStack` class; +the :method:`Symfony\\Component\\HttpFoundation\\Request::hasPreviousSession` method tells you if the request contains a session which was started in one of the previous requests. @@ -272,6 +284,10 @@ this complexity and defines some methods for the most common tasks:: HeaderUtils::unquote('"foo \"bar\""'); // => 'foo "bar"' + // Parses a query string but maintains dots (PHP parse_str() replaces '.' by '_') + HeaderUtils::parseQuery('foo[bar.baz]=qux'); + // => ['foo' => ['bar.baz' => 'qux']] + Accessing ``Accept-*`` Headers Data ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -319,10 +335,6 @@ are also supported:: Anonymizing IP Addresses ~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 4.4 - - The ``anonymize()`` method was introduced in Symfony 4.4. - An increasingly common need for applications to comply with user protection regulations is to anonymize IP addresses before logging and storing them for analysis purposes. Use the ``anonymize()`` method from the @@ -447,9 +459,17 @@ method takes an instance of You can clear a cookie via the :method:`Symfony\\Component\\HttpFoundation\\ResponseHeaderBag::clearCookie` method. -Note you can create a -:class:`Symfony\\Component\\HttpFoundation\\Cookie` object from a raw header -value using :method:`Symfony\\Component\\HttpFoundation\\Cookie::fromString`. +In addition to the ``Cookie::create()`` method, you can create a ``Cookie`` +object from a raw header value using :method:`Symfony\\Component\\HttpFoundation\\Cookie::fromString` +method. You can also use the ``with*()`` methods to change some Cookie property (or +to build the entire Cookie using a fluent interface). Each ``with*()`` method returns +a new object with the modified property:: + + $cookie = Cookie::create('foo') + ->withValue('bar') + ->withExpires(strtotime('Fri, 20-May-2011 15:25:52 GMT')) + ->withDomain('.example.com') + ->withSecure(true); Managing the HTTP Cache ~~~~~~~~~~~~~~~~~~~~~~~ @@ -463,6 +483,8 @@ of methods to manipulate the HTTP headers related to the cache: * :method:`Symfony\\Component\\HttpFoundation\\Response::setExpires` * :method:`Symfony\\Component\\HttpFoundation\\Response::setMaxAge` * :method:`Symfony\\Component\\HttpFoundation\\Response::setSharedMaxAge` +* :method:`Symfony\\Component\\HttpFoundation\\Response::setStaleIfError` +* :method:`Symfony\\Component\\HttpFoundation\\Response::setStaleWhileRevalidate` * :method:`Symfony\\Component\\HttpFoundation\\Response::setTtl` * :method:`Symfony\\Component\\HttpFoundation\\Response::setClientTtl` * :method:`Symfony\\Component\\HttpFoundation\\Response::setLastModified` @@ -481,15 +503,27 @@ can be used to set the most commonly used cache information in one method call:: $response->setCache([ - 'etag' => 'abcdef', - 'last_modified' => new \DateTime(), - 'max_age' => 600, - 's_maxage' => 600, - 'private' => false, - 'public' => true, - 'immutable' => true, + 'must_revalidate' => false, + 'no_cache' => false, + 'no_store' => false, + 'no_transform' => false, + 'public' => true, + 'private' => false, + 'proxy_revalidate' => false, + 'max_age' => 600, + 's_maxage' => 600, + 'stale_if_error' => 86400, + 'stale_while_revalidate' => 60, + 'immutable' => true, + 'last_modified' => new \DateTime(), + 'etag' => 'abcdef', ]); +.. versionadded:: 6.1 + + The ``stale_if_error`` and ``stale_while_revalidate`` options were + introduced in Symfony 6.1. + To check if the Response validators (``ETag``, ``Last-Modified``) match a conditional value specified in the client Request, use the :method:`Symfony\\Component\\HttpFoundation\\Response::isNotModified` @@ -684,7 +718,7 @@ The ``JsonResponse`` class sets the ``Content-Type`` header to .. caution:: To avoid XSSI `JSON Hijacking`_, you should pass an associative array - as the outer-most array to ``JsonResponse`` and not an indexed array so + as the outermost array to ``JsonResponse`` and not an indexed array so that the final result is an object (e.g. ``{"object": "not inside an array"}``) instead of an array (e.g. ``[{"object": "inside an array"}]``). Read the `OWASP guidelines`_ for more information. @@ -712,6 +746,65 @@ Session The session information is in its own document: :doc:`/components/http_foundation/sessions`. +Safe Content Preference +----------------------- + +Some web sites have a "safe" mode to assist those who don't want to be exposed +to content to which they might object. The `RFC 8674`_ specification defines a +way for user agents to ask for safe content to a server. + +The specification does not define what content might be considered objectionable, +so the concept of "safe" is not precisely defined. Rather, the term is interpreted +by the server and within the scope of each web site that chooses to act upon this information. + +Symfony offers two methods to interact with this preference: + +* :method:`Symfony\\Component\\HttpFoundation\\Request::preferSafeContent`; +* :method:`Symfony\\Component\\HttpFoundation\\Response::setContentSafe`; + +The following example shows how to detect if the user agent prefers "safe" content:: + + if ($request->preferSafeContent()) { + $response = new Response($alternativeContent); + // this informs the user we respected their preferences + $response->setContentSafe(); + + return $response; + +Generating Relative and Absolute URLs +------------------------------------- + +Generating absolute and relative URLs for a given path is a common need +in some applications. In Twig templates you can use the +:ref:`absolute_url() ` and +:ref:`relative_path() ` functions to do that. + +The :class:`Symfony\\Component\\HttpFoundation\\UrlHelper` class provides the +same functionality for PHP code via the ``getAbsoluteUrl()`` and ``getRelativePath()`` +methods. You can inject this as a service anywhere in your application:: + + // src/Normalizer/UserApiNormalizer.php + namespace App\Normalizer; + + use Symfony\Component\HttpFoundation\UrlHelper; + + class UserApiNormalizer + { + private UrlHelper $urlHelper; + + public function __construct(UrlHelper $urlHelper) + { + $this->urlHelper = $urlHelper; + } + + public function normalize($user) + { + return [ + 'avatar' => $this->urlHelper->getAbsoluteUrl($user->avatar()->path()), + ]; + } + } + Learn More ---------- @@ -729,3 +822,4 @@ Learn More .. _Apache: https://tn123.org/mod_xsendfile/ .. _`JSON Hijacking`: https://haacked.com/archive/2009/06/25/json-hijacking.aspx/ .. _OWASP guidelines: https://cheatsheetseries.owasp.org/cheatsheets/AJAX_Security_Cheat_Sheet.html#always-return-json-with-an-object-on-the-outside +.. _RFC 8674: https://tools.ietf.org/html/rfc8674 diff --git a/components/http_foundation/sessions.rst b/components/http_foundation/sessions.rst index e8ca28d35d3..197693f32ac 100644 --- a/components/http_foundation/sessions.rst +++ b/components/http_foundation/sessions.rst @@ -166,9 +166,6 @@ and "Remember Me" login settings or other user based state information. :class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBag` This is the standard default implementation. -:class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\NamespacedAttributeBag` - This implementation allows for attributes to be stored in a structured namespace. - :class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface` has the API @@ -237,15 +234,6 @@ So any processing of this might quickly get ugly, even adding a token to the arr $tokens['c'] = $value; $session->set('tokens', $tokens); -With structured namespacing, the key can be translated to the array -structure like this using a namespace character (which defaults to ``/``):: - - // ... - use Symfony\Component\HttpFoundation\Session\Attribute\NamespacedAttributeBag; - - $session = new Session(new NativeSessionStorage(), new NamespacedAttributeBag()); - $session->set('tokens/c', $value); - Flash Messages ~~~~~~~~~~~~~~ diff --git a/components/http_kernel.rst b/components/http_kernel.rst index 0bdb8c24b2f..75eda8865c8 100644 --- a/components/http_kernel.rst +++ b/components/http_kernel.rst @@ -9,7 +9,7 @@ The HttpKernel Component The HttpKernel component provides a structured process for converting a ``Request`` into a ``Response`` by making use of the EventDispatcher component. It's flexible enough to create a full-stack framework (Symfony), - a micro-framework (Silex) or an advanced CMS system (Drupal). + a micro-framework (Silex) or an advanced CMS (Drupal). Installation ------------ @@ -65,9 +65,9 @@ that system:: */ public function handle( Request $request, - $type = self::MASTER_REQUEST, - $catch = true - ); + int $type = self::MAIN_REQUEST, + bool $catch = true + ): Response; } Internally, :method:`HttpKernel::handle() ` - @@ -289,7 +289,15 @@ After the controller callable has been determined, ``HttpKernel::handle()`` dispatches the ``kernel.controller`` event. Listeners to this event might initialize some part of the system that needs to be initialized after certain things have been determined (e.g. the controller, routing information) but before -the controller is executed. For some examples, see the Symfony section below. +the controller is executed. + +Another typical use-case for this event is to retrieve the attributes from +the controller using the :method:`Symfony\\Component\\HttpKernel\\Event\\ControllerEvent::getAttributes` +method. See the Symfony section below for some examples. + +.. versionadded:: 6.2 + + The ``ControllerEvent::getAttributes()`` method was introduced in Symfony 6.2. Listeners to this event can also change the controller callable completely by calling :method:`ControllerEvent::setController ` @@ -297,18 +305,15 @@ on the event object that's passed to listeners on this event. .. sidebar:: ``kernel.controller`` in the Symfony Framework - There are a few minor listeners to the ``kernel.controller`` event in - the Symfony Framework, and many deal with collecting profiler data when - the profiler is enabled. + An interesting listener to ``kernel.controller`` in the Symfony + Framework is :class:`Symfony\\Component\\HttpKernel\\EventListener\\CacheAttributeListener`. + This class fetches ``#[Cache]`` attribute configuration from the + controller and uses it to configure :doc:`HTTP caching ` + on the response. - One interesting listener comes from the `SensioFrameworkExtraBundle`_. This - listener's `@ParamConverter`_ functionality allows you to pass a full object - (e.g. a ``Post`` object) to your controller instead of a scalar value (e.g. - an ``id`` parameter that was on your route). The listener - - ``ParamConverterListener`` - uses reflection to look at each of the - arguments of the controller and tries to use different methods to convert - those to objects, which are then stored in the ``attributes`` property of - the ``Request`` object. Read the next section to see why this is important. + There are a few other minor listeners to the ``kernel.controller`` event in + the Symfony Framework that deal with collecting profiler data when the + profiler is enabled. 4) Getting the Controller Arguments ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -409,12 +414,12 @@ return a ``Response``. .. sidebar:: ``kernel.view`` in the Symfony Framework - There is no default listener inside the Symfony Framework for the ``kernel.view`` - event. However, `SensioFrameworkExtraBundle`_ *does* add a listener to this - event. If your controller returns an array, and you place the `@Template`_ - annotation above the controller, then this listener renders a template, - passes the array you returned from your controller to that template, and - creates a ``Response`` containing the returned content from that template. + There is a default listener inside the Symfony Framework for the ``kernel.view`` + event. If your controller action returns an array, and you apply the + :ref:`#[Template()] attribute ` to that + controller action, then this listener renders a template, passes the array + you returned from your controller to that template, and creates a ``Response`` + containing the returned content from that template. Additionally, a popular community bundle `FOSRestBundle`_ implements a listener on this event which aims to give you a robust view layer @@ -524,22 +529,10 @@ object, which you can use to access the original exception via the method. A typical listener on this event will check for a certain type of exception and create an appropriate error ``Response``. -.. versionadded:: 4.4 - - The :method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::getThrowable` and - :method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::setThrowable` methods - were introduced in Symfony 4.4. - -.. deprecated:: 4.4 - - The :method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::getException` and - :method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::setException` methods - are deprecated since Symfony 4.4. - For example, to generate a 404 page, you might throw a special type of exception and then add a listener on this event that looks for this exception and creates and returns a 404 ``Response``. In fact, the HttpKernel component -comes with an :class:`Symfony\\Component\\HttpKernel\\EventListener\\ExceptionListener`, +comes with an :class:`Symfony\\Component\\HttpKernel\\EventListener\\ErrorListener`, which if you choose to use, will do this and more by default (see the sidebar below for more details). @@ -553,10 +546,10 @@ below for more details). There are two main listeners to ``kernel.exception`` when using the Symfony Framework. - **ExceptionListener in the HttpKernel Component** + **ErrorListener in the HttpKernel Component** The first comes core to the HttpKernel component - and is called :class:`Symfony\\Component\\HttpKernel\\EventListener\\ExceptionListener`. + and is called :class:`Symfony\\Component\\HttpKernel\\EventListener\\ErrorListener`. The listener has several goals: 1) The thrown exception is converted into a @@ -621,19 +614,6 @@ kernel.terminate ``KernelEvents::TERMINATE`` :class:`Sym kernel.exception ``KernelEvents::EXCEPTION`` :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` =========================== ====================================== ======================================================================== -.. deprecated:: 4.3 - - Since Symfony 4.3, most of the event classes were renamed. - The following old classes were deprecated: - - * `GetResponseEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\RequestEvent` - * `FilterControllerEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\ControllerEvent` - * `FilterControllerArgumentsEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\ControllerArgumentsEvent` - * `GetResponseForControllerResultEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\ViewEvent` - * `FilterResponseEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\ResponseEvent` - * `PostResponseEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\TerminateEvent` - * `GetResponseForExceptionEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` - .. _http-kernel-working-example: A full Working Example @@ -720,12 +700,12 @@ argument as follows:: This creates another full request-response cycle where this new ``Request`` is transformed into a ``Response``. The only difference internally is that some -listeners (e.g. security) may only act upon the master request. Each listener +listeners (e.g. security) may only act upon the main request. Each listener is passed some subclass of :class:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent`, -whose :method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::isMasterRequest` -can be used to check if the current request is a "master" or "sub" request. +whose :method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::isMainRequest` +method can be used to check if the current request is a "main" or "sub" request. -For example, a listener that only needs to act on the master request may +For example, a listener that only needs to act on the main request may look like this:: use Symfony\Component\HttpKernel\Event\RequestEvent; @@ -733,7 +713,7 @@ look like this:: public function onKernelRequest(RequestEvent $event) { - if (!$event->isMasterRequest()) { + if (!$event->isMainRequest()) { return; } @@ -774,7 +754,4 @@ Learn more .. _reflection: https://www.php.net/manual/en/book.reflection.php .. _FOSRestBundle: https://github.com/friendsofsymfony/FOSRestBundle .. _`PHP FPM`: https://www.php.net/manual/en/install.fpm.php -.. _`SensioFrameworkExtraBundle`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html -.. _`@ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html -.. _`@Template`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/view.html .. _variadic: https://www.php.net/manual/en/functions.arguments.php#functions.variable-arg-list diff --git a/components/inflector.rst b/components/inflector.rst deleted file mode 100644 index 960cb04d4ba..00000000000 --- a/components/inflector.rst +++ /dev/null @@ -1,64 +0,0 @@ -.. index:: - single: Inflector - single: Components; Inflector - -The Inflector Component -======================= - - The Inflector component converts English words between their singular and - plural forms. - -Installation ------------- - -.. code-block:: terminal - - $ composer require symfony/inflector - -.. include:: /components/require_autoload.rst.inc - -When you May Need an Inflector ------------------------------- - -In some scenarios such as code generation and code introspection, it's usually -required to convert words from/to singular/plural. For example, if you need to -know which property is associated with an *adder* method, you must convert from -plural to singular (``addStories()`` method -> ``$story`` property). - -Although most human languages define simple pluralization rules, they also -define lots of exceptions. For example, the general rule in English is to add an -``s`` at the end of the word (``book`` -> ``books``) but there are lots of -exceptions even for common words (``woman`` -> ``women``, ``life`` -> ``lives``, -``news`` -> ``news``, ``radius`` -> ``radii``, etc.) - -This component abstracts all those pluralization rules so you can convert -from/to singular/plural with confidence. However, due to the complexity of the -human languages, this component only provides support for the English language. - -Usage ------ - -The Inflector component provides two static methods to convert from/to -singular/plural:: - - use Symfony\Component\Inflector\Inflector; - - Inflector::singularize('alumni'); // 'alumnus' - Inflector::singularize('knives'); // 'knife' - Inflector::singularize('mice'); // 'mouse' - - Inflector::pluralize('grandchild'); // 'grandchildren' - Inflector::pluralize('news'); // 'news' - Inflector::pluralize('bacterium'); // 'bacteria' - -Sometimes it's not possible to determine a unique singular/plural form for the -given word. In those cases, the methods return an array with all the possible -forms:: - - use Symfony\Component\Inflector\Inflector; - - Inflector::singularize('indices'); // ['index', 'indix', 'indice'] - Inflector::singularize('leaves'); // ['leaf', 'leave', 'leaff'] - - Inflector::pluralize('matrix'); // ['matrices', 'matrixes'] - Inflector::pluralize('person'); // ['persons', 'people'] diff --git a/components/intl.rst b/components/intl.rst index 387956f85e3..c4dd2aba3df 100644 --- a/components/intl.rst +++ b/components/intl.rst @@ -6,7 +6,6 @@ The Intl Component ================== This component provides access to the localization data of the `ICU library`_. - It also provides a PHP replacement layer for the C `intl extension`_. .. caution:: @@ -30,30 +29,6 @@ Installation .. include:: /components/require_autoload.rst.inc -If you install the component via Composer, the following classes and functions -of the intl extension will be automatically provided if the intl extension is -not loaded: - -* :phpclass:`Collator` -* :phpclass:`IntlDateFormatter` -* :phpclass:`Locale` -* :phpclass:`NumberFormatter` -* :phpfunction:`intl_error_name` -* :phpfunction:`intl_is_failure` -* :phpfunction:`intl_get_error_code` -* :phpfunction:`intl_get_error_message` - -When the intl extension is not available, the following classes are used to -replace the intl classes: - -* :class:`Symfony\\Component\\Intl\\Collator\\Collator` -* :class:`Symfony\\Component\\Intl\\DateFormatter\\IntlDateFormatter` -* :class:`Symfony\\Component\\Intl\\Locale\\Locale` -* :class:`Symfony\\Component\\Intl\\NumberFormatter\\NumberFormatter` -* :class:`Symfony\\Component\\Intl\\Globals\\IntlGlobals` - -Composer automatically exposes these classes in the global namespace. - Accessing ICU Data ------------------ @@ -64,6 +39,7 @@ This component provides the following ICU data: * `Locales`_ * `Currencies`_ * `Timezones`_ +* `Emoji Transliteration`_ Language and Script Names ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -120,14 +96,6 @@ You may convert codes between two-letter alpha2 and three-letter alpha3 codes:: $alpha2Code = Languages::getAlpha2Code($alpha3Code); -.. versionadded:: 4.3 - - The ``Languages`` class was introduced in Symfony 4.3. - -.. versionadded:: 4.4 - - The full support for alpha3 codes was introduced in Symfony 4.4. - The :class:`Symfony\\Component\\Intl\\Scripts` class provides access to the optional four-letter script code that can follow the language code according to the `Unicode ISO 15924 Registry`_ (e.g. ``HANS`` in ``zh_HANS`` for simplified Chinese and ``HANT`` in ``zh_HANT`` @@ -159,10 +127,6 @@ to catching the exception, you can also check if a given script code is valid:: $isValidScript = Scripts::exists($scriptCode); -.. versionadded:: 4.3 - - The ``Scripts`` class was introduced in Symfony 4.3. - Country Names ~~~~~~~~~~~~~ @@ -219,14 +183,6 @@ You may convert codes between two-letter alpha2 and three-letter alpha3 codes:: $alpha2Code = Countries::getAlpha2Code($alpha3Code); -.. versionadded:: 4.3 - - The ``Countries`` class was introduced in Symfony 4.3. - -.. versionadded:: 4.4 - - The support for alpha3 codes was introduced in Symfony 4.4. - Locales ~~~~~~~ @@ -262,10 +218,6 @@ to catching the exception, you can also check if a given locale code is valid:: $isValidLocale = Locales::exists($localeCode); -.. versionadded:: 4.3 - - The ``Locales`` class was introduced in Symfony 4.3. - Currencies ~~~~~~~~~~ @@ -286,15 +238,37 @@ of all currencies as well as some of their information (symbol, fraction digits, $symbol = Currencies::getSymbol('INR'); // => '₹' - $fractionDigits = Currencies::getFractionDigits('INR'); - // => 2 +The fraction digits methods return the number of decimal digits to display when +formatting numbers with this currency. Depending on the currency, this value +can change if the number is used in cash transactions or in other scenarios +(e.g. accounting):: + + // Indian rupee defines the same value for both + $fractionDigits = Currencies::getFractionDigits('INR'); // returns: 2 + $cashFractionDigits = Currencies::getCashFractionDigits('INR'); // returns: 2 - $roundingIncrement = Currencies::getRoundingIncrement('INR'); - // => 0 + // Swedish krona defines different values + $fractionDigits = Currencies::getFractionDigits('SEK'); // returns: 2 + $cashFractionDigits = Currencies::getCashFractionDigits('SEK'); // returns: 0 -All methods (except for ``getFractionDigits()`` and ``getRoundingIncrement()``) -accept the translation locale as the last, optional parameter, which defaults to -the current default locale:: +Some currencies require to round numbers to the nearest increment of some value +(e.g. 5 cents). This increment might be different if numbers are formatted for +cash transactions or other scenarios (e.g. accounting):: + + // Indian rupee defines the same value for both + $roundingIncrement = Currencies::getRoundingIncrement('INR'); // returns: 0 + $cashRoundingIncrement = Currencies::getCashRoundingIncrement('INR'); // returns: 0 + + // Canadian dollar defines different values because they have eliminated + // the smaller coins (1-cent and 2-cent) and prices in cash must be rounded to + // 5 cents (e.g. if price is 7.42 you pay 7.40; if price is 7.48 you pay 7.50) + $roundingIncrement = Currencies::getRoundingIncrement('CAD'); // returns: 0 + $cashRoundingIncrement = Currencies::getCashRoundingIncrement('CAD'); // returns: 5 + +All methods (except for ``getFractionDigits()``, ``getCashFractionDigits()``, +``getRoundingIncrement()`` and ``getCashRoundingIncrement()``) accept the +translation locale as the last, optional parameter, which defaults to the +current default locale:: $currencies = Currencies::getNames('de'); // => ['AFN' => 'Afghanischer Afghani', 'EGP' => 'Ägyptisches Pfund', ...] @@ -308,10 +282,6 @@ to catching the exception, you can also check if a given currency code is valid: $isValidCurrency = Currencies::exists($currencyCode); -.. versionadded:: 4.3 - - The ``Currencies`` class was introduced in Symfony 4.3. - .. _component-intl-timezones: Timezones @@ -391,9 +361,50 @@ to catching the exception, you can also check if a given timezone ID is valid:: $isValidTimezone = Timezones::exists($timezoneId); -.. versionadded:: 4.3 +.. _component-intl-emoji-transliteration: + +Emoji Transliteration +~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 6.2 + + The Emoji transliteration feature was introduced in Symfony 6.2. + +The ``EmojiTransliterator`` class provides a utility to translate emojis into +their textual representation in all languages based on the `Unicode CLDR dataset`_:: + + use Symfony\Component\Intl\Transliterator\EmojiTransliterator; + + // describe emojis in English + $transliterator = EmojiTransliterator::create('en'); + $transliterator->transliterate('Menus with 🍕 or 🍝'); + // => 'Menus with pizza or spaghetti' + + // describe emojis in Ukrainian + $transliterator = EmojiTransliterator::create('uk'); + $transliterator->transliterate('Menus with 🍕 or 🍝'); + // => 'Menus with піца or спагеті' + +The ``EmojiTransliterator`` class also provides two extra catalogues: ``github`` +and ``slack`` that converts any emojis to the corresponding short code in those +platforms:: + + use Symfony\Component\Intl\Transliterator\EmojiTransliterator; + + // describe emojis in Slack short code + $transliterator = EmojiTransliterator::create('slack'); + $transliterator->transliterate('Menus with 🥗 or 🧆'); + // => 'Menus with :green_salad: or :falafel:' + + // describe emojis in Github short code + $transliterator = EmojiTransliterator::create('github'); + $transliterator->transliterate('Menus with 🥗 or 🧆'); + // => 'Menus with :green_salad: or :falafel:' + +.. tip:: - The ``Timezones`` class was introduced in Symfony 4.3. + Combine this emoji transliterator with the :ref:`Symfony String slugger ` + to improve the slugs of contents that include emojis (e.g. for URLs). Learn more ---------- @@ -408,7 +419,6 @@ Learn more /reference/forms/types/locale /reference/forms/types/timezone -.. _intl extension: https://www.php.net/manual/en/book.intl.php .. _install the intl extension: https://www.php.net/manual/en/intl.setup.php .. _ICU library: http://site.icu-project.org/ .. _`Unicode ISO 15924 Registry`: https://www.unicode.org/iso15924/iso15924-codes.html @@ -418,3 +428,4 @@ Learn more .. _`daylight saving time (DST)`: https://en.wikipedia.org/wiki/Daylight_saving_time .. _`ISO 639-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_639-1 .. _`ISO 639-2 alpha-3 (2T)`: https://en.wikipedia.org/wiki/ISO_639-2 +.. _`Unicode CLDR dataset`: https://github.com/unicode-org/cldr diff --git a/components/ldap.rst b/components/ldap.rst index 0c164d305dc..21ef7a8bcfb 100644 --- a/components/ldap.rst +++ b/components/ldap.rst @@ -146,6 +146,9 @@ delete existing ones:: $phoneNumber = $entry->getAttribute('phoneNumber'); $isContractor = $entry->hasAttribute('contractorCompany'); + // attribute names in getAttribute() and hasAttribute() methods are case-sensitive + // pass FALSE as the second method argument to make them case-insensitive + $isContractor = $entry->hasAttribute('contractorCompany', false); $entry->setAttribute('email', ['fabpot@symfony.com']); $entryManager->update($entry); diff --git a/components/lock.rst b/components/lock.rst index cd783ff6d94..2b430f75de5 100644 --- a/components/lock.rst +++ b/components/lock.rst @@ -36,11 +36,6 @@ which in turn requires another class to manage the storage of locks:: $store = new SemaphoreStore(); $factory = new LockFactory($store); -.. versionadded:: 4.4 - - The ``Symfony\Component\Lock\LockFactory`` class was introduced in Symfony - 4.4. In previous versions it was called ``Symfony\Component\Lock\Factory``. - The lock is created by calling the :method:`Symfony\\Component\\Lock\\LockFactory::createLock` method. Its first argument is an arbitrary string that represents the locked resource. Then, a call to the :method:`Symfony\\Component\\Lock\\LockInterface::acquire` @@ -112,20 +107,24 @@ can be created, pass ``true`` as the argument of the ``acquire()`` method. This is called a **blocking lock** because the execution of your application stops until the lock is acquired. -Some of the built-in ``Store`` classes support this feature. When they don't, -they can be decorated with the ``RetryTillSaveStore`` class:: +Some of the built-in ``Store`` classes support this feature:: use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\Store\RedisStore; - use Symfony\Component\Lock\Store\RetryTillSaveStore; $store = new RedisStore(new \Predis\Client('tcp://localhost:6379')); - $store = new RetryTillSaveStore($store); $factory = new LockFactory($store); $lock = $factory->createLock('notification-flush'); $lock->acquire(true); +When the provided store does not implement the +:class:`Symfony\\Component\\Lock\\BlockingStoreInterface` interface, the +``Lock`` class will retry to acquire the lock in a non-blocking way until the +lock is acquired. However, the ``Lock`` class also provides the default logic to +acquire locks in blocking mode when the store does not implement the +``BlockingStoreInterface`` interface. + Expiring Locks -------------- @@ -204,7 +203,7 @@ as seconds) and ``isExpired()`` (which returns a boolean). Automatically Releasing The Lock ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Locks are automatically released when their Lock objects are destructed. This is +Locks are automatically released when their Lock objects are destroyed. This is an implementation detail that will be important when sharing Locks between processes. In the example below, ``pcntl_fork()`` creates two processes and the Lock will be released automatically as soon as one process finishes:: @@ -233,6 +232,58 @@ To disable this behavior, set to ``false`` the third argument of ``LockFactory::createLock()``. That will make the lock acquired for 3600 seconds or until ``Lock::release()`` is called. +Shared Locks +------------ + +A shared or `readers–writer lock`_ is a synchronization primitive that allows +concurrent access for read-only operations, while write operations require +exclusive access. This means that multiple threads can read the data in parallel +but an exclusive lock is needed for writing or modifying data. They are used for +example for data structures that cannot be updated atomically and are invalid +until the update is complete. + +Use the :method:`Symfony\\Component\\Lock\\SharedLockInterface::acquireRead` method +to acquire a read-only lock, and the existing +:method:`Symfony\\Component\\Lock\\LockInterface::acquire` method to acquire a +write lock:: + + $lock = $factory->createLock('user'.$user->id); + if (!$lock->acquireRead()) { + return; + } + +Similar to the ``acquire()`` method, pass ``true`` as the argument of ``acquireRead()`` +to acquire the lock in a blocking mode:: + + $lock = $factory->createLock('user'.$user->id); + $lock->acquireRead(true); + +.. note:: + + The `priority policy`_ of Symfony's shared locks depends on the underlying + store (e.g. Redis store prioritizes readers vs writers). + +When a read-only lock is acquired with the method ``acquireRead()``, it's +possible to **promote** the lock, and change it to write lock, by calling the +``acquire()`` method:: + + $lock = $factory->createLock('user'.$userId); + $lock->acquireRead(true); + + if (!$this->shouldUpdate($userId)) { + return; + } + + $lock->acquire(true); // Promote the lock to write lock + $this->update($userId); + +In the same way, it's possible to **demote** a write lock, and change it to a +read-only lock by calling the ``acquireRead()`` method. + +When the provided store does not implement the +:class:`Symfony\\Component\\Lock\\SharedLockStoreInterface` interface, the +``Lock`` class will fallback to a write lock by calling the ``acquire()`` method. + The Owner of The Lock --------------------- @@ -287,22 +338,25 @@ Locks are created and managed in ``Stores``, which are classes that implement The component includes the following built-in store types: -============================================ ====== ======== ======== -Store Scope Blocking Expiring -============================================ ====== ======== ======== -:ref:`FlockStore ` local yes no -:ref:`MemcachedStore ` remote no yes -:ref:`PdoStore ` remote no yes -:ref:`RedisStore ` remote no yes -:ref:`SemaphoreStore ` local yes no -:ref:`ZookeeperStore ` remote no no -============================================ ====== ======== ======== +========================================================== ====== ======== ======== ======= +Store Scope Blocking Expiring Sharing +========================================================== ====== ======== ======== ======= +:ref:`FlockStore ` local yes no yes +:ref:`MemcachedStore ` remote no yes no +:ref:`MongoDbStore ` remote no yes no +:ref:`PdoStore ` remote no yes no +:ref:`DoctrineDbalStore ` remote no yes no +:ref:`PostgreSqlStore ` remote yes no yes +:ref:`DoctrineDbalPostgreSqlStore ` remote yes no yes +:ref:`RedisStore ` remote no yes yes +:ref:`SemaphoreStore ` local yes no no +:ref:`ZookeeperStore ` remote no no no +========================================================== ====== ======== ======== ======= -.. versionadded:: 4.4 +.. tip:: - The ``PersistingStoreInterface`` and ``BlockingStoreInterface`` interfaces were - introduced in Symfony 4.4. In previous versions there was only one interface - called ``Symfony\Component\Lock\StoreInterface``. + A special ``InMemoryStore`` is available for saving locks in memory during + a process, and can be useful for testing. .. _lock-store-flock: @@ -346,44 +400,150 @@ support blocking, and expects a TTL to avoid stalled locks:: Memcached does not support TTL lower than 1 second. +.. _lock-store-mongodb: + +MongoDbStore +~~~~~~~~~~~~ + +The MongoDbStore saves locks on a MongoDB server ``>=2.2``, it requires a +``\MongoDB\Collection`` or ``\MongoDB\Client`` from `mongodb/mongodb`_ or a +`MongoDB Connection String`_. +This store does not support blocking and expects a TTL to +avoid stalled locks:: + + use Symfony\Component\Lock\Store\MongoDbStore; + + $mongo = 'mongodb://localhost/database?collection=lock'; + $options = [ + 'gcProbablity' => 0.001, + 'database' => 'myapp', + 'collection' => 'lock', + 'uriOptions' => [], + 'driverOptions' => [], + ]; + $store = new MongoDbStore($mongo, $options); + +The ``MongoDbStore`` takes the following ``$options`` (depending on the first parameter type): + +============= ================================================================================================ +Option Description +============= ================================================================================================ +gcProbablity Should a TTL Index be created expressed as a probability from 0.0 to 1.0 (Defaults to ``0.001``) +database The name of the database +collection The name of the collection +uriOptions Array of uri options for `MongoDBClient::__construct`_ +driverOptions Array of driver options for `MongoDBClient::__construct`_ +============= ================================================================================================ + +When the first parameter is a: + +``MongoDB\Collection``: + +- ``$options['database']`` is ignored +- ``$options['collection']`` is ignored + +``MongoDB\Client``: + +- ``$options['database']`` is mandatory +- ``$options['collection']`` is mandatory + +MongoDB Connection String: + +- ``$options['database']`` is used otherwise ``/path`` from the DSN, at least one is mandatory +- ``$options['collection']`` is used otherwise ``?collection=`` from the DSN, at least one is mandatory + +.. note:: + + The ``collection`` querystring parameter is not part of the `MongoDB Connection String`_ definition. + It is used to allow constructing a ``MongoDbStore`` using a `Data Source Name (DSN)`_ without ``$options``. + .. _lock-store-pdo: PdoStore ~~~~~~~~ -The PdoStore saves locks in an SQL database. It requires a `PDO`_ connection, a -`Doctrine DBAL Connection`_, or a `Data Source Name (DSN)`_. This store does not -support blocking, and expects a TTL to avoid stalled locks:: +The PdoStore saves locks in an SQL database. It is identical to DoctrineDbalStore +but requires a `PDO`_ connection or a `Data Source Name (DSN)`_. This store does +not support blocking, and expects a TTL to avoid stalled locks:: use Symfony\Component\Lock\Store\PdoStore; - // a PDO, a Doctrine DBAL connection or DSN for lazy connecting through PDO - $databaseConnectionOrDSN = 'mysql:host=127.0.0.1;dbname=lock'; + // a PDO or DSN for lazy connecting through PDO + $databaseConnectionOrDSN = 'mysql:host=127.0.0.1;dbname=app'; $store = new PdoStore($databaseConnectionOrDSN, ['db_username' => 'myuser', 'db_password' => 'mypassword']); .. note:: This store does not support TTL lower than 1 second. -Before storing locks in the database, you must create the table that stores -the information. The store provides a method called -:method:`Symfony\\Component\\Lock\\Store\\PdoStore::createTable` -to set up this table for you according to the database engine used:: +The table where values are stored is created automatically on the first call to +the :method:`Symfony\\Component\\Lock\\Store\\PdoStore::save` method. +You can also create this table explicitly by calling the +:method:`Symfony\\Component\\Lock\\Store\\PdoStore::createTable` method in +your code. - try { - $store->createTable(); - } catch (\PDOException $exception) { - // the table could not be created for some reason - } +.. _lock-store-dbal: -A great way to set up the table in production is to call the ``createTable()`` -method in your local computer and then generate a -:ref:`database migration `: +DoctrineDbalStore +~~~~~~~~~~~~~~~~~ -.. code-block:: terminal +The DoctrineDbalStore saves locks in an SQL database. It is identical to PdoStore +but requires a `Doctrine DBAL Connection`_, or a `Doctrine DBAL URL`_. This store +does not support blocking, and expects a TTL to avoid stalled locks:: - $ php bin/console doctrine:migrations:diff - $ php bin/console doctrine:migrations:migrate + use Symfony\Component\Lock\Store\DoctrineDbalStore; + + // a Doctrine DBAL connection or DSN + $connectionOrURL = 'mysql://myuser:mypassword@127.0.0.1/app'; + $store = new DoctrineDbalStore($connectionOrURL); + +.. note:: + + This store does not support TTL lower than 1 second. + +The table where values are stored is created automatically on the first call to +the :method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::save` method. +You can also add this table to your schema by calling +:method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::configureSchema` method +in your code or create this table explicitly by calling the +:method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::createTable` method. + +.. _lock-store-pgsql: + +PostgreSqlStore +~~~~~~~~~~~~~~~ + +The PostgreSqlStore and DoctrineDbalPostgreSqlStore uses `Advisory Locks`_ provided by PostgreSQL. +It is identical to DoctrineDbalPostgreSqlStore but requires `PDO`_ connection or +a `Data Source Name (DSN)`_. It supports native blocking, as well as sharing +locks:: + + use Symfony\Component\Lock\Store\PostgreSqlStore; + + // a PDO instance or DSN for lazy connecting through PDO + $databaseConnectionOrDSN = 'pgsql:host=localhost;port=5634;dbname=lock'; + $store = new PostgreSqlStore($databaseConnectionOrDSN, ['db_username' => 'myuser', 'db_password' => 'mypassword']); + +In opposite to the ``PdoStore``, the ``PostgreSqlStore`` does not need a table to +store locks and it does not expire. + +.. _lock-store-dbal-pgsql: + +DoctrineDbalPostgreSqlStore +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The DoctrineDbalPostgreSqlStore uses `Advisory Locks`_ provided by PostgreSQL. +It is identical to PostgreSqlStore but requires a `Doctrine DBAL Connection`_ or +a `Doctrine DBAL URL`_. It supports native blocking, as well as sharing locks:: + + use Symfony\Component\Lock\Store\DoctrineDbalPostgreSqlStore; + + // a Doctrine Connection or DSN + $databaseConnectionOrDSN = 'postgresql+advisory://myuser:mypassword@127.0.0.1:5634/lock'; + $store = new DoctrineDbalPostgreSqlStore($databaseConnectionOrDSN); + +In opposite to the ``DoctrineDbalStore``, the ``DoctrineDbalPostgreSqlStore`` does not need a table to +store locks and does not expire. .. _lock-store-redis: @@ -482,7 +642,9 @@ Remote Stores ~~~~~~~~~~~~~ Remote stores (:ref:`MemcachedStore `, +:ref:`MongoDbStore `, :ref:`PdoStore `, +:ref:`PostgreSqlStore `, :ref:`RedisStore ` and :ref:`ZookeeperStore `) use a unique token to recognize the true owner of the lock. This token is stored in the @@ -502,6 +664,7 @@ Expiring Stores ~~~~~~~~~~~~~~~ Expiring stores (:ref:`MemcachedStore `, +:ref:`MongoDbStore `, :ref:`PdoStore ` and :ref:`RedisStore `) guarantee that the lock is acquired only for the defined duration of time. If @@ -622,6 +785,47 @@ method uses the Memcached's ``flush()`` method which purges and removes everythi The method ``flush()`` must not be called, or locks should be stored in a dedicated Memcached service away from Cache. +MongoDbStore +~~~~~~~~~~~~ + +.. caution:: + + The locked resource name is indexed in the ``_id`` field of the lock + collection. Beware that in MongoDB an indexed field's value can be + `a maximum of 1024 bytes in length`_ inclusive of structural overhead. + +A TTL index must be used to automatically clean up expired locks. +Such an index can be created manually: + +.. code-block:: javascript + + db.lock.createIndex( + { "expires_at": 1 }, + { "expireAfterSeconds": 0 } + ) + +Alternatively, the method ``MongoDbStore::createTtlIndex(int $expireAfterSeconds = 0)`` +can be called once to create the TTL index during database setup. Read more +about `Expire Data from Collections by Setting TTL`_ in MongoDB. + +.. tip:: + + ``MongoDbStore`` will attempt to automatically create a TTL index. + It's recommended to set constructor option ``gcProbablity = 0.0`` to + disable this behavior if you have manually dealt with TTL index creation. + +.. caution:: + + This store relies on all PHP application and database nodes to have + synchronized clocks for lock expiry to occur at the correct time. To ensure + locks don't expire prematurely; the lock TTL should be set with enough extra + time in ``expireAfterSeconds`` to account for any clock drift between nodes. + +``writeConcern`` and ``readConcern`` are not specified by MongoDbStore meaning +the collection's settings will take effect. +``readPreference`` is ``primary`` for all queries. +Read more about `Replica Set Read and Write Semantics`_ in MongoDB. + PdoStore ~~~~~~~~~~ @@ -646,6 +850,20 @@ have synchronized clocks. To ensure locks don't expire prematurely; the TTLs should be set with enough extra time to account for any clock drift between nodes. +PostgreSqlStore +~~~~~~~~~~~~~~~ + +The PdoStore relies on the `Advisory Locks`_ properties of the PostgreSQL +database. That means that by using :ref:`PostgreSqlStore ` +the locks will be automatically released at the end of the session in case the +client cannot unlock for any reason. + +If the PostgreSQL service or the machine hosting it restarts, every lock would +be lost without notifying the running processes. + +If the TCP connection is lost, the PostgreSQL may release locks without +notifying the application. + RedisStore ~~~~~~~~~~ @@ -748,10 +966,20 @@ instance, during the deployment of a new version. Processes with new configuration must not be started while old processes with old configuration are still running. +.. _`a maximum of 1024 bytes in length`: https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit .. _`ACID`: https://en.wikipedia.org/wiki/ACID +.. _`Advisory Locks`: https://www.postgresql.org/docs/current/explicit-locking.html +.. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name +.. _`Doctrine DBAL Connection`: https://github.com/doctrine/dbal/blob/master/src/Connection.php +.. _`Doctrine DBAL URL`: https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +.. _`Expire Data from Collections by Setting TTL`: https://docs.mongodb.com/manual/tutorial/expire-data/ .. _`locks`: https://en.wikipedia.org/wiki/Lock_(computer_science) -.. _`PHP semaphore functions`: https://www.php.net/manual/en/book.sem.php +.. _`MongoDB Connection String`: https://docs.mongodb.com/manual/reference/connection-string/ +.. _`mongodb/mongodb`: https://packagist.org/packages/mongodb/mongodb +.. _`MongoDBClient::__construct`: https://docs.mongodb.com/php-library/current/reference/method/MongoDBClient__construct/ .. _`PDO`: https://www.php.net/pdo -.. _`Doctrine DBAL Connection`: https://github.com/doctrine/dbal/blob/master/lib/Doctrine/DBAL/Connection.php -.. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name +.. _`PHP semaphore functions`: https://www.php.net/manual/en/book.sem.php +.. _`Replica Set Read and Write Semantics`: https://docs.mongodb.com/manual/applications/replication/ .. _`ZooKeeper`: https://zookeeper.apache.org/ +.. _`readers–writer lock`: https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock +.. _`priority policy`: https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock#Priority_policies diff --git a/components/messenger.rst b/components/messenger.rst index b71680ff70e..21b8cabac3e 100644 --- a/components/messenger.rst +++ b/components/messenger.rst @@ -77,15 +77,9 @@ middleware stack. The component comes with a set of middleware that you can use. When using the message bus with Symfony's FrameworkBundle, the following middleware are configured for you: -#. :class:`Symfony\\Component\\Messenger\\Middleware\\LoggingMiddleware` (logs the processing of your messages) -#. :class:`Symfony\\Component\\Messenger\\Middleware\\SendMessageMiddleware` (enables asynchronous processing) +#. :class:`Symfony\\Component\\Messenger\\Middleware\\SendMessageMiddleware` (enables asynchronous processing, logs the processing of your messages if you provide a logger) #. :class:`Symfony\\Component\\Messenger\\Middleware\\HandleMessageMiddleware` (calls the registered handler(s)) -.. deprecated:: 4.3 - - The ``LoggingMiddleware`` is deprecated since Symfony 4.3 and will be - removed in 5.0. Pass a logger to ``SendMessageMiddleware`` instead. - Example:: use App\Message\MyMessage; @@ -168,6 +162,8 @@ Here are some important envelope stamps that are shipped with the Symfony Messen to configure the serialization groups used by the transport. #. :class:`Symfony\\Component\\Messenger\\Stamp\\ValidationStamp`, to configure the validation groups used when the validation middleware is enabled. +#. :class:`Symfony\\Component\\Messenger\\Stamp\\ErrorDetailsStamp`, + an internal stamp when a message fails due to an exception in the handler. Instead of dealing directly with the messages in the middleware you receive the envelope. Hence you can inspect the envelope content and its stamps, or add any:: @@ -332,12 +328,6 @@ do is to write your own CSV receiver:: } } -.. versionadded:: 4.3 - - In Symfony 4.3, the ``ReceiverInterface`` has changed its methods as shown - in the example above. You may need to update your code if you used this - interface in previous Symfony versions. - Receiver and Sender on the same Bus ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/components/mime.rst b/components/mime.rst index 965c7a377ae..a641283716e 100644 --- a/components/mime.rst +++ b/components/mime.rst @@ -9,10 +9,6 @@ The Mime Component The Mime component allows manipulating the MIME messages used to send emails and provides utilities related to MIME types. -.. versionadded:: 4.3 - - The Mime component was introduced in Symfony 4.3. - Installation ------------ diff --git a/components/options_resolver.rst b/components/options_resolver.rst index 4d556d86fda..7a499045aa8 100644 --- a/components/options_resolver.rst +++ b/components/options_resolver.rst @@ -335,6 +335,8 @@ is thrown:: In sub-classes, you can use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addAllowedTypes` to add additional allowed types without erasing the ones already set. +.. _optionsresolver-validate-value: + Value Validation ~~~~~~~~~~~~~~~~ @@ -376,6 +378,21 @@ returns ``true`` for acceptable values and ``false`` for invalid values:: // return true or false }); +.. tip:: + + You can even use the :doc:`Validator ` component to validate the + input by using the :method:`Symfony\\Component\\Validator\\Validation::createIsValidCallable` + method:: + + use Symfony\Component\OptionsResolver\OptionsResolver; + use Symfony\Component\Validator\Constraints\Length; + use Symfony\Component\Validator\Validation; + + // ... + $resolver->setAllowedValues('transport', Validation::createIsValidCallable( + new Length(['min' => 10 ]) + )); + In sub-classes, you can use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addAllowedValues` to add additional allowed values without erasing the ones already set. @@ -440,10 +457,6 @@ This way, the ``$value`` argument will receive the previously normalized value, otherwise you can prepend the new normalizer by passing ``true`` as third argument. -.. versionadded:: 4.3 - - The ``addNormalizer()`` method was introduced in Symfony 4.3. - Default Values that Depend on another Option ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -721,6 +734,51 @@ In same way, parent options can access to the nested options as normal arrays:: The fact that an option is defined as nested means that you must pass an array of values to resolve it at runtime. +Prototype Options +~~~~~~~~~~~~~~~~~ + +There are situations where you will have to resolve and validate a set of +options that may repeat many times within another option. Let's imagine a +``connections`` option that will accept an array of database connections +with ``host``, ``database``, ``user`` and ``password`` each. + +The best way to implement this is to define the ``connections`` option as prototype:: + + $resolver->setDefault('connections', function (OptionsResolver $connResolver) { + $connResolver + ->setPrototype(true) + ->setRequired(['host', 'database']) + ->setDefaults(['user' => 'root', 'password' => null]); + }); + +According to the prototype definition in the example above, it is possible +to have multiple connection arrays like the following:: + + $resolver->resolve([ + 'connections' => [ + 'default' => [ + 'host' => '127.0.0.1', + 'database' => 'symfony', + ], + 'test' => [ + 'host' => '127.0.0.1', + 'database' => 'symfony_test', + 'user' => 'test', + 'password' => 'test', + ], + // ... + ], + ]); + +The array keys (``default``, ``test``, etc.) of this prototype option are +validation-free and can be any arbitrary value that helps differentiate the +connections. + +.. note:: + + A prototype option can only be defined inside a nested option and + during its resolution it will expect an array of arrays. + Deprecating the Option ~~~~~~~~~~~~~~~~~~~~~~ @@ -730,12 +788,18 @@ method:: $resolver ->setDefined(['hostname', 'host']) - // this outputs the following generic deprecation message: - // The option "hostname" is deprecated. - ->setDeprecated('hostname') - // you can also pass a custom deprecation message - ->setDeprecated('hostname', 'The option "hostname" is deprecated, use "host" instead.') + // this outputs the following generic deprecation message: + // Since acme/package 1.2: The option "hostname" is deprecated. + ->setDeprecated('hostname', 'acme/package', '1.2') + + // you can also pass a custom deprecation message (%name% placeholder is available) + ->setDeprecated( + 'hostname', + 'acme/package', + '1.2', + 'The option "hostname" is deprecated, use "host" instead.' + ) ; .. note:: @@ -760,7 +824,7 @@ the option:: ->setDefault('encryption', null) ->setDefault('port', null) ->setAllowedTypes('port', ['null', 'int']) - ->setDeprecated('port', function (Options $options, $value) { + ->setDeprecated('port', 'acme/package', '1.2', function (Options $options, $value) { if (null === $value) { return 'Passing "null" to option "port" is deprecated, pass an integer instead.'; } @@ -782,6 +846,36 @@ the option:: This closure receives as argument the value of the option after validating it and before normalizing it when the option is being resolved. +Chaining Option Configurations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In many cases you may need to define multiple configurations for each option. +For example, suppose the ``InvoiceMailer`` class has an ``host`` option that is required +and a ``transport`` option which can be one of ``sendmail``, ``mail`` and ``smtp``. +You can improve the readability of the code avoiding to duplicate option name for +each configuration using the :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::define` +method:: + + // ... + class InvoiceMailer + { + // ... + public function configureOptions(OptionsResolver $resolver) + { + // ... + $resolver->define('host') + ->required() + ->default('smtp.example.org') + ->allowedTypes('string') + ->info('The IP address or hostname'); + + $resolver->define('transport') + ->required() + ->default('transport') + ->allowedValues('sendmail', 'mail', 'smtp'); + } + } + Performance Tweaks ~~~~~~~~~~~~~~~~~~ diff --git a/components/phpunit_bridge.rst b/components/phpunit_bridge.rst index cb77a1e376f..8650153e378 100644 --- a/components/phpunit_bridge.rst +++ b/components/phpunit_bridge.rst @@ -161,18 +161,14 @@ each test suite's results in their own section. Trigger Deprecation Notices --------------------------- -Deprecation notices can be triggered by using:: +Deprecation notices can be triggered by using ``trigger_deprecation`` from +the ``symfony/deprecation-contracts`` package:: - @trigger_error('Your deprecation message', E_USER_DEPRECATED); + // indicates something is deprecated since version 1.3 of vendor-name/packagename + trigger_deprecation('vendor-name/package-name', '1.3', 'Your deprecation message'); -You can also require the ``symfony/deprecation-contracts`` package that provides -a global ``trigger_deprecation()`` function for this usage. - -Without the `@-silencing operator`_, users would need to opt-out from deprecation -notices. Silencing by default swaps this behavior and allows users to opt-in -when they are ready to cope with them (by adding a custom error handler like the -one provided by this bridge). When not silenced, deprecation notices will appear -in the **Unsilenced** section of the deprecation report. + // you can also use printf format (all arguments after the message will be used) + trigger_deprecation('...', '1.3', 'Value "%s" is deprecated, use ... instead.', $value); Mark Tests as Legacy -------------------- @@ -293,6 +289,59 @@ Here is a summary that should help you pick the right configuration: | | cannot afford to use one of the modes above. | +------------------------+-----------------------------------------------------+ +Ignoring Deprecations +..................... + +.. versionadded:: 6.1 + + The ``ignoreFile`` feature was introduced in Symfony 6.1. + +If your application has some deprecations that you can't fix for some reasons, +you can tell Symfony to ignore them. + +You need first to create a text file where each line is a deprecation to ignore +defined as a regular expression. Lines beginning with a hash (``#``) are +considered comments: + +.. code-block:: terminal + + # This file contains patterns to be ignored while testing for use of + # deprecated code. + + %The "Symfony\\Component\\Validator\\Context\\ExecutionContextInterface::.*\(\)" method is considered internal Used by the validator engine\. (Should not be called by user\W+code\. )?It may change without further notice\. You should not extend it from "[^"]+"\.% + %The "PHPUnit\\Framework\\TestCase::addWarning\(\)" method is considered internal% + +Then, you can run the following command to use that file and ignore those deprecations: + +.. code-block:: terminal + + $ SYMFONY_DEPRECATIONS_HELPER='ignoreFile=./tests/baseline-ignore' ./vendor/bin/simple-phpunit + +Baseline Deprecations +..................... + +You can also take a snapshot of deprecations currently triggered by your application +code, and ignore those during your test runs, still reporting newly added ones. +The trick is to create a file with the allowed deprecations and define it as the +"deprecation baseline". Deprecations inside that file are ignored but the rest of +deprecations are still reported. + +First, generate the file with the allowed deprecations (run the same command +whenever you want to update the existing file): + +.. code-block:: terminal + + $ SYMFONY_DEPRECATIONS_HELPER='generateBaseline=true&baselineFile=./tests/allowed.json' ./vendor/bin/simple-phpunit + +This command stores all the deprecations reported while running tests in the +given file path and encoded in JSON. + +Then, you can run the following command to use that file and ignore those deprecations: + +.. code-block:: terminal + + $ SYMFONY_DEPRECATIONS_HELPER='baselineFile=./tests/allowed.json' ./vendor/bin/simple-phpunit + Disabling the Verbose Output ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -300,6 +349,10 @@ By default, the bridge will display a detailed output with the number of deprecations and where they arise. If this is too much for you, you can use ``SYMFONY_DEPRECATIONS_HELPER=verbose=0`` to turn the verbose output off. +It's also possible to change verbosity per deprecation type. For example, using +``quiet[]=indirect&quiet[]=other`` will hide details for deprecations of types +"indirect" and "other". + Disabling the Deprecation Helper ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -330,9 +383,21 @@ time. This can be disabled with the ``debug-class-loader`` option. -.. versionadded:: 4.2 +Compile-time Deprecations +~~~~~~~~~~~~~~~~~~~~~~~~~ - The ``DebugClassLoader`` integration was introduced in Symfony 4.2. +Use the ``debug:container`` command to list the deprecations generated during +the compiling and warming up of the container: + +.. code-block:: terminal + + $ php bin/console debug:container --deprecations + +Log Deprecations +~~~~~~~~~~~~~~~~ + +For turning the verbose output off and write it to a log file instead you can use +``SYMFONY_DEPRECATIONS_HELPER='logFile=/path/deprecations.log'``. .. _write-assertions-about-deprecations: @@ -341,21 +406,34 @@ Write Assertions about Deprecations When adding deprecations to your code, you might like writing tests that verify that they are triggered as required. To do so, the bridge provides the -``@expectedDeprecation`` annotation that you can use on your test methods. +``expectDeprecation()`` method that you can use on your test methods. It requires you to pass the expected message, given in the same format as for the `PHPUnit's assertStringMatchesFormat()`_ method. If you expect more than one -deprecation message for a given test method, you can use the annotation several +deprecation message for a given test method, you can use the method several times (order matters):: - /** - * @group legacy - * @expectedDeprecation This "%s" method is deprecated. - * @expectedDeprecation The second argument of the "%s" method is deprecated. - */ - public function testDeprecatedCode() + use PHPUnit\Framework\TestCase; + use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; + + class MyTest extends TestCase { - @trigger_error('This "Foo" method is deprecated.', E_USER_DEPRECATED); - @trigger_error('The second argument of the "Bar" method is deprecated.', E_USER_DEPRECATED); + use ExpectDeprecationTrait; + + /** + * @group legacy + */ + public function testDeprecatedCode() + { + // test some code that triggers the following deprecation: + // trigger_deprecation('vendor-name/package-name', '5.1', 'This "Foo" method is deprecated.'); + $this->expectDeprecation('Since vendor-name/package-name 5.1: This "%s" method is deprecated'); + + // ... + + // test some code that triggers the following deprecation: + // trigger_deprecation('vendor-name/package-name', '4.4', 'The second argument of the "Bar" method is deprecated.'); + $this->expectDeprecation('Since vendor-name/package-name 4.4: The second argument of the "%s" method is deprecated.'); + } } Display the Full Stack Trace @@ -394,10 +472,6 @@ the test suite cannot use the latest versions of PHPUnit because: Polyfills for the Unavailable Methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 4.4 - - This feature was introduced in Symfony 4.4. - When using the ``simple-phpunit`` script, PHPUnit Bridge injects polyfills for most methods of the ``TestCase`` and ``Assert`` classes (e.g. ``expectException()``, ``expectExceptionMessage()``, ``assertContainsEquals()``, etc.). This allows writing @@ -407,49 +481,15 @@ older PHPUnit versions. Removing the Void Return Type ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 4.4 - - This feature was introduced in Symfony 4.4. - When running the ``simple-phpunit`` script with the ``SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT`` environment variable set to ``1``, the PHPUnit bridge will alter the code of PHPUnit to remove the return type (introduced in PHPUnit 8) from ``setUp()``, ``tearDown()``, ``setUpBeforeClass()`` and ``tearDownAfterClass()`` methods. This allows you to write a test compatible with both PHP 5 and PHPUnit 8. -Alternatively, you can use the trait :class:`Symfony\\Bridge\\PhpUnit\\SetUpTearDownTrait`, -which provides the right signature for the ``setUp()``, ``tearDown()``, -``setUpBeforeClass()`` and ``tearDownAfterClass()`` methods and delegates the -call to the ``doSetUp()``, ``doTearDown()``, ``doSetUpBeforeClass()`` and -``doTearDownAfterClass()`` methods:: - - use PHPUnit\Framework\TestCase; - use Symfony\Bridge\PhpUnit\SetUpTearDownTrait; - - class MyTest extends TestCase - { - // when using the SetUpTearDownTrait, methods like doSetUp() can - // be defined with and without the 'void' return type, as you wish - use SetUpTearDownTrait; - - private function doSetUp() - { - // ... - } - - protected function doSetUp(): void - { - // ... - } - } - Using Namespaced PHPUnit Classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 4.4 - - This feature was introduced in Symfony 4.4. - The PHPUnit bridge adds namespaced class aliases for most of the PHPUnit classes declared without namespaces (e.g. ``PHPUnit_Framework_Assert``), allowing you to always use the namespaced class declaration even when the test is executed with @@ -494,8 +534,13 @@ Clock Mocking The :class:`Symfony\\Bridge\\PhpUnit\\ClockMock` class provided by this bridge allows you to mock the PHP's built-in time functions ``time()``, ``microtime()``, -``sleep()``, ``usleep()`` and ``gmdate()``. Additionally the function ``date()`` -is mocked so it uses the mocked time if no timestamp is specified. +``sleep()``, ``usleep()``, ``gmdate()``, and ``hrtime()`. Additionally the +function ``date()`` is mocked so it uses the mocked time if no timestamp is +specified. + +.. versionadded:: 6.2 + + Support for mocking the ``hrtime()`` function was introduced in Symfony 6.2. Other functions with an optional timestamp parameter that defaults to ``time()`` will still use the system time instead of the mocked time. This means that you @@ -619,20 +664,19 @@ functions: Use Case ~~~~~~~~ -Consider the following example that uses the ``checkMX`` option of the ``Email`` -constraint to test the validity of the email domain:: +Consider the following example that tests a custom class called ``DomainValidator`` +which defines a ``checkDnsRecord`` option to also validate that a domain is +associated to a valid host:: + use App\Validator\DomainValidator; use PHPUnit\Framework\TestCase; - use Symfony\Component\Validator\Constraints\Email; class MyTest extends TestCase { public function testEmail() { - $validator = ...; - $constraint = new Email(['checkMX' => true]); - - $result = $validator->validate('foo@example.com', $constraint); + $validator = new DomainValidator(['checkDnsRecord' => true]); + $isValid = $validator->validate('example.com'); // ... } @@ -642,22 +686,23 @@ In order to avoid making a real network connection, add the ``@group dns-sensiti annotation to the class and use the ``DnsMock::withMockedHosts()`` to configure the data you expect to get for the given hosts:: + use App\Validator\DomainValidator; use PHPUnit\Framework\TestCase; - use Symfony\Component\Validator\Constraints\Email; + use Symfony\Bridge\PhpUnit\DnsMock; /** * @group dns-sensitive */ - class MyTest extends TestCase + class DomainValidatorTest extends TestCase { public function testEmails() { - DnsMock::withMockedHosts(['example.com' => [['type' => 'MX']]]); - - $validator = ...; - $constraint = new Email(['checkMX' => true]); + DnsMock::withMockedHosts([ + 'example.com' => [['type' => 'A', 'ip' => '1.2.3.4']], + ]); - $result = $validator->validate('foo@example.com', $constraint); + $validator = new DomainValidator(['checkDnsRecord' => true]); + $isValid = $validator->validate('example.com'); // ... } @@ -860,6 +905,10 @@ If you have installed the bridge through Composer, you can run it by calling e.g It's also possible to set ``SYMFONY_PHPUNIT_VERSION`` as a real env var (not defined in a :ref:`dotenv file `). + In the same way, ``SYMFONY_MAX_PHPUNIT_VERSION`` will set the maximum version + of PHPUnit to be considered. This is useful when testing a framework that does + not support the latest version(s) of PHPUnit. + .. tip:: If you still need to use ``prophecy`` (but not ``symfony/yaml``), @@ -867,6 +916,13 @@ If you have installed the bridge through Composer, you can run it by calling e.g It's also possible to set this env var in the ``phpunit.xml.dist`` file. +.. tip:: + + It is also possible to require additional packages that will be installed along + the rest of the needed PHPUnit packages using the ``SYMFONY_PHPUNIT_REQUIRE`` + env variable. This is specially useful for installing PHPUnit plugins without + having to add them to your main ``composer.json`` file. + Code Coverage Listener ---------------------- diff --git a/components/process.rst b/components/process.rst index d8d585fe987..19be88a706b 100644 --- a/components/process.rst +++ b/components/process.rst @@ -102,10 +102,16 @@ with a non-zero code):: :method:`Symfony\\Component\\Process\\Process::getLastOutputTime` method. This method returns ``null`` if the process wasn't started! - .. versionadded:: 4.4 +Configuring Process Options +--------------------------- - The :method:`Symfony\\Component\\Process\\Process::getLastOutputTime` - method was introduced in Symfony 4.4. +Symfony uses the PHP :phpfunction:`proc_open` function to run the processes. +You can configure the options passed to the ``other_options`` argument of +``proc_open()`` using the ``setOptions()`` method:: + + $process = new Process(['...', '...', '...']); + // this option allows a subprocess to continue running after the main script exited + $process->setOptions(['create_new_console' => true]); Using Features From the OS Shell -------------------------------- @@ -150,10 +156,6 @@ enclosing a variable name into ``"${:`` and ``}"`` exactly, the process object will replace it with its escaped value, or will fail if the variable is not found in the list of environment variables attached to the command. -.. versionadded:: 4.4 - - Portable command lines were introduced in Symfony 4.4. - Setting Environment Variables for Processes ------------------------------------------- @@ -442,6 +444,10 @@ check regularly:: usleep(200000); } +.. tip:: + + You can get the process start time using the ``getStartTime()`` method. + .. _reference-process-signal: Process Idle Timeout diff --git a/components/property_access.rst b/components/property_access.rst index f9375516cf8..fcd0551c257 100644 --- a/components/property_access.rst +++ b/components/property_access.rst @@ -5,7 +5,7 @@ The PropertyAccess Component ============================ - The PropertyAccess component provides function to read and write from/to an + The PropertyAccess component provides functions to read and write from/to an object or array using a simple string notation. Installation @@ -63,6 +63,9 @@ method:: // Symfony\Component\PropertyAccess\Exception\NoSuchIndexException $value = $propertyAccessor->getValue($person, '[age]'); + // You can avoid the exception by adding the nullsafe operator + $value = $propertyAccessor->getValue($person, '[age?]'); + You can also use multi dimensional arrays:: // ... @@ -169,11 +172,6 @@ This will produce: ``This person is an author`` Accessing a non Existing Property Path ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 4.3 - - The ``disableExceptionOnInvalidPropertyPath()`` method was introduced in - Symfony 4.3. - By default a :class:`Symfony\\Component\\PropertyAccess\\Exception\\NoSuchPropertyException` is thrown if the property path passed to :method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::getValue` does not exist. You can change this behavior using the @@ -195,6 +193,41 @@ method:: // instead of throwing an exception the following code returns null $value = $propertyAccessor->getValue($person, 'birthday'); +Accessing Nullable Property Paths +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Consider the following PHP code:: + + class Person + { + } + + class Comment + { + public ?Person $person = null; + public string $message; + } + + $comment = new Comment(); + $comment->message = 'test'; + +Given that ``$person`` is nullable, an object graph like ``comment.person.profile`` +will trigger an exception when the ``$person`` property is ``null``. The solution +is to mark all nullable properties with the nullsafe operator (``?``):: + + // This code throws an exception of type + // Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException + var_dump($propertyAccessor->getValue($comment, 'person.firstname')); + + // If a property marked with the nullsafe operator is null, the expression is + // no longer evaluated and null is returned immediately without throwing an exception + var_dump($propertyAccessor->getValue($comment, 'person?.firstname')); // null + +.. versionadded:: 6.2 + + The ``?`` nullsafe operator was introduced in Symfony 6.2. + +.. _components-property-access-magic-get: Magic ``__get()`` Method ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -218,6 +251,11 @@ The ``getValue()`` method can also use the magic ``__get()`` method:: var_dump($propertyAccessor->getValue($person, 'Wouter')); // [...] +.. note:: + + The ``__get()`` method support is enabled by default. + See `Enable other Features`_ if you want to disable it. + .. _components-property-access-magic-call: Magic ``__call()`` Method @@ -276,6 +314,8 @@ also write to an array. This can be achieved using the // or // var_dump($person['first_name']); // 'Wouter' +.. _components-property-access-writing-to-objects: + Writing to Objects ------------------ @@ -352,6 +392,11 @@ see `Enable other Features`_:: var_dump($person->getWouter()); // [...] +.. note:: + + The ``__set()`` method support is enabled by default. + See `Enable other Features`_ if you want to disable it. + Writing to Array Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -391,10 +436,49 @@ The PropertyAccess component checks for methods called ``add()``. Both methods must be defined. For instance, in the previous example, the component looks for the ``addChild()`` and ``removeChild()`` methods to access the ``children`` property. -`The Inflector component`_ is used to find the singular of a property name. +`The String component`_ inflector is used to find the singular of a property name. If available, *adder* and *remover* methods have priority over a *setter* method. +Using non-standard adder/remover methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes, adder and remover methods don't use the standard ``add`` or ``remove`` prefix, like in this example:: + + // ... + class PeopleList + { + // ... + + public function joinPeople(string $people): void + { + $this->peoples[] = $people; + } + + public function leavePeople(string $people): void + { + foreach ($this->peoples as $id => $item) { + if ($people === $item) { + unset($this->peoples[$id]); + break; + } + } + } + } + + use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; + use Symfony\Component\PropertyAccess\PropertyAccessor; + + $list = new PeopleList(); + $reflectionExtractor = new ReflectionExtractor(null, null, ['join', 'leave']); + $propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH, null, $reflectionExtractor, $reflectionExtractor); + $propertyAccessor->setValue($person, 'peoples', ['kevin', 'wouter']); + + var_dump($person->getPeoples()); // ['kevin', 'wouter'] + +Instead of calling ``add()`` and ``remove()``, the PropertyAccess +component will call ``join()`` and ``leave()`` methods. + Checking Property Paths ----------------------- @@ -462,14 +546,20 @@ configured to enable extra features. To do that you could use the // ... $propertyAccessorBuilder = PropertyAccess::createPropertyAccessorBuilder(); - // enables magic __call - $propertyAccessorBuilder->enableMagicCall(); + $propertyAccessorBuilder->enableMagicCall(); // enables magic __call + $propertyAccessorBuilder->enableMagicGet(); // enables magic __get + $propertyAccessorBuilder->enableMagicSet(); // enables magic __set + $propertyAccessorBuilder->enableMagicMethods(); // enables magic __get, __set and __call - // disables magic __call - $propertyAccessorBuilder->disableMagicCall(); + $propertyAccessorBuilder->disableMagicCall(); // disables magic __call + $propertyAccessorBuilder->disableMagicGet(); // disables magic __get + $propertyAccessorBuilder->disableMagicSet(); // disables magic __set + $propertyAccessorBuilder->disableMagicMethods(); // disables magic __get, __set and __call - // checks if magic __call handling is enabled + // checks if magic __call, __get or __set handling are enabled $propertyAccessorBuilder->isMagicCallEnabled(); // true or false + $propertyAccessorBuilder->isMagicGetEnabled(); // true or false + $propertyAccessorBuilder->isMagicSetEnabled(); // true or false // At the end get the configured property accessor $propertyAccessor = $propertyAccessorBuilder->getPropertyAccessor(); @@ -481,7 +571,7 @@ configured to enable extra features. To do that you could use the Or you can pass parameters directly to the constructor (not the recommended way):: - // ... - $propertyAccessor = new PropertyAccessor(true); // this enables handling of magic __call + // enable handling of magic __call, __set but not __get: + $propertyAccessor = new PropertyAccessor(PropertyAccessor::MAGIC_CALL | PropertyAccessor::MAGIC_SET); -.. _The Inflector component: https://github.com/symfony/inflector +.. _`The String component`: https://github.com/symfony/string diff --git a/components/property_info.rst b/components/property_info.rst index 8d86663c140..abddaad0ae1 100644 --- a/components/property_info.rst +++ b/components/property_info.rst @@ -122,7 +122,7 @@ class exposes public methods to extract several types of information: * :ref:`List of properties `: :method:`Symfony\\Component\\PropertyInfo\\PropertyListExtractorInterface::getProperties` * :ref:`Property type `: :method:`Symfony\\Component\\PropertyInfo\\PropertyTypeExtractorInterface::getTypes` - (including typed properties since PHP 7.4) + (including typed properties) * :ref:`Property description `: :method:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface::getShortDescription` and :method:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface::getLongDescription` * :ref:`Property access details `: :method:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface::isReadable` and :method:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface::isWritable` * :ref:`Property initializable through the constructor `: :method:`Symfony\\Component\\PropertyInfo\\PropertyInitializableExtractorInterface::isInitializable` @@ -293,10 +293,6 @@ string values: ``array``, ``bool``, ``callable``, ``float``, ``int``, Constants inside the :class:`Symfony\\Component\\PropertyInfo\\Type` class, in the form ``Type::BUILTIN_TYPE_*``, are provided for convenience. -.. versionadded:: 4.4 - - Support for typed properties (added in PHP 7.4) was introduced in Symfony 4.4. - ``Type::isNullable()`` ~~~~~~~~~~~~~~~~~~~~~~ @@ -327,10 +323,6 @@ this returns ``true`` if: ``@var SomeClass``, ``@var SomeClass``, ``@var Doctrine\Common\Collections\Collection``, etc.) -.. versionadded:: 4.2 - - The support of phpDocumentor collection types was introduced in Symfony 4.2. - ``Type::getCollectionKeyType()`` & ``Type::getCollectionValueType()`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -365,7 +357,7 @@ Using PHP reflection, the :class:`Symfony\\Component\\PropertyInfo\\Extractor\\R provides list, type and access information from setter and accessor methods. It can also give the type of a property (even extracting it from the constructor arguments), and if it is initializable through the constructor. It supports -return and scalar types for PHP 7:: +return and scalar types:: use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; @@ -384,11 +376,6 @@ return and scalar types for PHP 7:: // Initializable information $reflectionExtractor->isInitializable($class, $property); -.. versionadded:: 4.1 - - The feature to extract the property types from constructor arguments was - introduced in Symfony 4.1. - .. note:: When using the Symfony framework, this service is automatically registered @@ -424,6 +411,41 @@ library is present:: $phpDocExtractor->getShortDescription($class, $property); $phpDocExtractor->getLongDescription($class, $property); +PhpStanExtractor +~~~~~~~~~~~~~~~~ + +.. note:: + + This extractor depends on the `phpstan/phpdoc-parser`_ and + `phpdocumentor/reflection-docblock`_ libraries. + +This extractor fetches information thanks to the PHPStan parser. It gathers +information from annotations of properties and methods, such as ``@var``, +``@param`` or ``@return``:: + + // src/Domain/Foo.php + class Foo + { + private $bar; + + /** + * @param string $bar + */ + public function __construct($bar) { + $this->bar = $bar; + } + } + + // Extraction.php + use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; + + $phpStanExtractor = new PhpStanExtractor(); + $phpStanExtractor->getTypesFromConstructor(Foo::class, 'bar'); + +.. versionadded:: 6.1 + + The ``PhpStanExtractor`` was introduced in Symfony 6.1. + SerializerExtractor ~~~~~~~~~~~~~~~~~~~ @@ -431,7 +453,7 @@ SerializerExtractor This extractor depends on the `symfony/serializer`_ library. -Using :ref:`groups metadata ` +Using :ref:`groups metadata ` from the :doc:`Serializer component `, the :class:`Symfony\\Component\\PropertyInfo\\Extractor\\SerializerExtractor` provides list information. This extractor is *not* registered automatically @@ -447,8 +469,12 @@ with the ``property_info`` service in the Symfony Framework:: ); $serializerExtractor = new SerializerExtractor($serializerClassMetadataFactory); - // List information. - $serializerExtractor->getProperties($class); + // the `serializer_groups` option must be configured (may be set to null) + $serializerExtractor->getProperties($class, ['serializer_groups' => ['mygroup']]); + +If ``serializer_groups`` is set to ``null``, serializer groups metadata won't be +checked but you will get only the properties considered by the Serializer +Component (notably the ``@Ignore`` annotation is taken into account). DoctrineExtractor ~~~~~~~~~~~~~~~~~ @@ -504,12 +530,9 @@ service by defining it as a service with one or more of the following * ``property_info.initializable_extractor`` if it provides initializable information (it checks if a property can be initialized through the constructor). -.. versionadded:: 4.2 - - The ``property_info.initializable_extractor`` was introduced in Symfony 4.2. - .. _`phpDocumentor Reflection`: https://github.com/phpDocumentor/ReflectionDocBlock .. _`phpdocumentor/reflection-docblock`: https://packagist.org/packages/phpdocumentor/reflection-docblock +.. _`phpstan/phpdoc-parser`: https://packagist.org/packages/phpstan/phpdoc-parser .. _`Doctrine ORM`: https://www.doctrine-project.org/projects/orm.html .. _`symfony/serializer`: https://packagist.org/packages/symfony/serializer .. _`symfony/doctrine-bridge`: https://packagist.org/packages/symfony/doctrine-bridge diff --git a/components/runtime.rst b/components/runtime.rst new file mode 100644 index 00000000000..95a8f3be5a3 --- /dev/null +++ b/components/runtime.rst @@ -0,0 +1,491 @@ +.. index:: + single: Runtime + single: Components; Runtime + +The Runtime Component +====================== + + The Runtime Component decouples the bootstrapping logic from any global state + to make sure the application can run with runtimes like PHP-PM, ReactPHP, + Swoole, etc. without any changes. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/runtime + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +The Runtime component abstracts most bootstrapping logic as so-called +*runtimes*, allowing you to write front-controllers in a generic way. +For instance, the Runtime component allows Symfony's ``public/index.php`` +to look like this:: + + handle(Request::createFromGlobals())->send()``). + +.. caution:: + + If you use the Composer ``--no-plugins`` option, the ``autoload_runtime.php`` + file won't be created. + + If you use the Composer ``--no-scripts`` option, make sure your Composer version + is ``>=2.1.3``; otherwise the ``autoload_runtime.php`` file won't be created. + +To make a console application, the bootstrap code would look like:: + + #!/usr/bin/env php + setCode(function (InputInterface $input, OutputInterface $output) { + $output->write('Hello World'); + }); + + return $command; + }; + +:class:`Symfony\\Component\\Console\\Application` + Useful with console applications with more than one command. This will use the + :class:`Symfony\\Component\\Runtime\\Runner\\Symfony\\ConsoleApplicationRunner`:: + + setCode(function (InputInterface $input, OutputInterface $output) { + $output->write('Hello World'); + }); + + $app = new Application(); + $app->add($command); + $app->setDefaultCommand('hello', true); + + return $app; + }; + +The ``GenericRuntime`` and ``SymfonyRuntime`` also support these generic +applications: + +:class:`Symfony\\Component\\Runtime\\RunnerInterface` + The ``RunnerInterface`` is a way to use a custom application with the + generic Runtime:: + + '/var/task', + ]; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + // ... + +You can also configure ``extra.runtime`` in ``composer.json``: + +.. code-block:: json + + { + "require": { + "...": "..." + }, + "extra": { + "runtime": { + "project_dir": "/var/task" + } + } + } + +The following options are supported by the ``SymfonyRuntime``: + +``env`` (default: ``APP_ENV`` environment variable, or ``"dev"``) + To define the name of the environment the app runs in. +``disable_dotenv`` (default: ``false``) + To disable looking for ``.env`` files. +``dotenv_path`` (default: ``.env``) + To define the path of dot-env files. +``dotenv_overload`` (default: ``false``) + To tell Dotenv whether to override ``.env`` vars with ``.env.local`` (or other ``.env.*`` files) +``use_putenv`` + To tell Dotenv to set env vars using ``putenv()`` (NOT RECOMMENDED). +``prod_envs`` (default: ``["prod"]``) + To define the names of the production envs. +``test_envs`` (default: ``["test"]``) + To define the names of the test envs. + +Besides these, the ``GenericRuntime`` and ``SymfonyRuntime`` also support +these options: + +``debug`` (default: the value of the env var defined by ``debug_var_name`` option + (usually, ``APP_DEBUG``), or ``true`` if such env var is not defined) + Toggles the :ref:`debug mode ` of Symfony applications (e.g. to + display errors) +``runtimes`` + Maps "application types" to a ``GenericRuntime`` implementation that + knows how to deal with each of them. +``error_handler`` (default: :class:`Symfony\\Component\\Runtime\\Internal\\BasicErrorHandler` or :class:`Symfony\\Component\\Runtime\\Internal\\SymfonyErrorHandler` for ``SymfonyRuntime``) + Defines the class to use to handle PHP errors. +``env_var_name`` (default: ``"APP_ENV"``) + Defines the name of the env var that stores the name of the + :ref:`configuration environment ` + to use when running the application. +``debug_var_name`` (default: ``"APP_DEBUG"``) + Defines the name of the env var that stores the value of the + :ref:`debug mode ` flag to use when running the application. + +Create Your Own Runtime +----------------------- + +This is an advanced topic that describes the internals of the Runtime component. + +Using the Runtime component will benefit maintainers because the bootstrap +logic could be versioned as a part of a normal package. If the application +author decides to use this component, the package maintainer of the Runtime +class will have more control and can fix bugs and add features. + +The Runtime component is designed to be totally generic and able to run any +application outside of the global state in 6 steps: + +#. The main entry point returns a *callable* (the "app") that wraps the application; +#. The *app callable* is passed to ``RuntimeInterface::getResolver()``, which returns + a :class:`Symfony\\Component\\Runtime\\ResolverInterface`. This resolver returns + an array with the app callable (or something that decorates this callable) at + index 0 and all its resolved arguments at index 1. +#. The *app callable* is invoked with its arguments, it will return an object that + represents the application. +#. This *application object* is passed to ``RuntimeInterface::getRunner()``, which + returns a :class:`Symfony\\Component\\Runtime\\RunnerInterface`: an instance + that knows how to "run" the application object. +#. The ``RunnerInterface::run(object $application)`` is called and it returns the + exit status code as `int`. +#. The PHP engine is terminated with this status code. + +When creating a new runtime, there are two things to consider: First, what arguments +will the end user use? Second, what will the user's application look like? + +For instance, imagine you want to create a runtime for `ReactPHP`_: + +**What arguments will the end user use?** + +For a generic ReactPHP application, no special arguments are +typically required. This means that you can use the +:class:`Symfony\\Component\\Runtime\\GenericRuntime`. + +**What will the user's application look like?** + +There is also no typical React application, so you might want to rely on +the `PSR-15`_ interfaces for HTTP request handling. + +However, a ReactPHP application will need some special logic to *run*. That logic +is added in a new class implementing :class:`Symfony\\Component\\Runtime\\RunnerInterface`:: + + use Psr\Http\Message\ServerRequestInterface; + use Psr\Http\Server\RequestHandlerInterface; + use React\EventLoop\Factory as ReactFactory; + use React\Http\Server as ReactHttpServer; + use React\Socket\Server as ReactSocketServer; + use Symfony\Component\Runtime\RunnerInterface; + + class ReactPHPRunner implements RunnerInterface + { + private $application; + private $port; + + public function __construct(RequestHandlerInterface $application, int $port) + { + $this->application = $application; + $this->port = $port; + } + + public function run(): int + { + $application = $this->application; + $loop = ReactFactory::create(); + + // configure ReactPHP to correctly handle the PSR-15 application + $server = new ReactHttpServer( + $loop, + function (ServerRequestInterface $request) use ($application) { + return $application->handle($request); + } + ); + + // start the ReactPHP server + $socket = new ReactSocketServer($this->port, $loop); + $server->listen($socket); + + $loop->run(); + + return 0; + } + } + +By extending the ``GenericRuntime``, you make sure that the application is +always using this ``ReactPHPRunner``:: + + use Symfony\Component\Runtime\GenericRuntime; + use Symfony\Component\Runtime\RunnerInterface; + + class ReactPHPRuntime extends GenericRuntime + { + private $port; + + public function __construct(array $options) + { + $this->port = $options['port'] ?? 8080; + parent::__construct($options); + } + + public function getRunner(?object $application): RunnerInterface + { + if ($application instanceof RequestHandlerInterface) { + return new ReactPHPRunner($application, $this->port); + } + + // if it's not a PSR-15 application, use the GenericRuntime to + // run the application (see "Resolvable Applications" above) + return parent::getRunner($application); + } + } + +The end user will now be able to create front controller like:: + + `:: - - 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\Authentication\Token\UsernamePasswordToken; - - class SomeAuthenticationListener - { - /** - * @var TokenStorageInterface - */ - private $tokenStorage; - - /** - * @var AuthenticationManagerInterface - */ - private $authenticationManager; - - /** - * @var string Uniquely identifies the secured area - */ - private $providerKey; - - // ... - - public function __invoke(RequestEvent $event) - { - $request = $event->getRequest(); - - $username = ...; - $password = ...; - - $unauthenticatedToken = new UsernamePasswordToken( - $username, - $password, - $this->providerKey - ); - - $authenticatedToken = $this - ->authenticationManager - ->authenticate($unauthenticatedToken); - - $this->tokenStorage->setToken($authenticatedToken); - } - } - -.. note:: - - A token can be of any class, as long as it implements - :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface`. - -The Authentication Manager --------------------------- - -The default authentication manager is an instance of -:class:`Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationProviderManager`:: - - use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager; - use Symfony\Component\Security\Core\Exception\AuthenticationException; - - // instances of Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface - $providers = [...]; - - $authenticationManager = new AuthenticationProviderManager($providers); - - try { - $authenticatedToken = $authenticationManager - ->authenticate($unauthenticatedToken); - } catch (AuthenticationException $exception) { - // authentication failed - } - -The ``AuthenticationProviderManager``, when instantiated, receives several -authentication providers, each supporting a different type of token. - -.. note:: - - You may write your own authentication manager, the only requirement is that - it implements :class:`Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationManagerInterface`. - -.. _authentication_providers: - -Authentication Providers ------------------------- - -Each provider (since it implements -:class:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface`) -has a :method:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface::supports` method -by which the ``AuthenticationProviderManager`` -can determine if it supports the given token. If this is the case, the -manager then calls the provider's :method:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface::authenticate` method. -This method should return an authenticated token or throw an -:class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException` -(or any other exception extending it). - -Authenticating Users by their Username and Password -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An authentication provider will attempt to authenticate a user based on -the credentials they provided. Usually these are a username and a password. -Most web applications store their user's username and a hash of the user's -password combined with a randomly generated salt. This means that the average -authentication would consist of fetching the salt and the hashed password -from the user data storage, hash the password the user has just provided -(e.g. using a login form) with the salt and compare both to determine if -the given password is valid. - -This functionality is offered by the :class:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\DaoAuthenticationProvider`. -It fetches the user's data from a :class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`, -uses a :class:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface` -to create a hash of the password and returns an authenticated token if the -password was valid:: - - use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider; - use Symfony\Component\Security\Core\Encoder\EncoderFactory; - use Symfony\Component\Security\Core\User\InMemoryUserProvider; - use Symfony\Component\Security\Core\User\UserChecker; - - $userProvider = new InMemoryUserProvider( - [ - 'admin' => [ - // password is "foo" - 'password' => '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg==', - 'roles' => ['ROLE_ADMIN'], - ], - ] - ); - - // for some extra checks: is account enabled, locked, expired, etc. - $userChecker = new UserChecker(); - - // an array of password encoders (see below) - $encoderFactory = new EncoderFactory(...); - - $daoProvider = new DaoAuthenticationProvider( - $userProvider, - $userChecker, - 'secured_area', - $encoderFactory - ); - - $daoProvider->authenticate($unauthenticatedToken); - -.. note:: - - The example above demonstrates the use of the "in-memory" user provider, - but you may use any user provider, as long as it implements - :class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`. - It is also possible to let multiple user providers try to find the user's - data, using the :class:`Symfony\\Component\\Security\\Core\\User\\ChainUserProvider`. - -The Password Encoder Factory -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\DaoAuthenticationProvider` -uses an encoder factory to create a password encoder for a given type of -user. This allows you to use different encoding strategies for different -types of users. The default :class:`Symfony\\Component\\Security\\Core\\Encoder\\EncoderFactory` -receives an array of encoders:: - - use Acme\Entity\LegacyUser; - use Symfony\Component\Security\Core\Encoder\EncoderFactory; - use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder; - use Symfony\Component\Security\Core\User\User; - - $defaultEncoder = new MessageDigestPasswordEncoder('sha512', true, 5000); - $weakEncoder = new MessageDigestPasswordEncoder('md5', true, 1); - - $encoders = [ - User::class => $defaultEncoder, - LegacyUser::class => $weakEncoder, - // ... - ]; - $encoderFactory = new EncoderFactory($encoders); - -Each encoder should implement :class:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface` -or be an array with a ``class`` and an ``arguments`` key, which allows the -encoder factory to construct the encoder only when it is needed. - -Creating a custom Password Encoder -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There are many built-in password encoders. But if you need to create your -own, it needs to follow these rules: - -#. The class must implement :class:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface` - (you can also extend :class:`Symfony\\Component\\Security\\Core\\Encoder\\BasePasswordEncoder`); - -#. The implementations of - :method:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface::encodePassword` - and - :method:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface::isPasswordValid` - must first of all make sure the password is not too long, i.e. the password length is no longer - than 4096 characters. This is for security reasons (see `CVE-2013-5750`_), and you can use the - :method:`Symfony\\Component\\Security\\Core\\Encoder\\BasePasswordEncoder::isPasswordTooLong` - method for this check:: - - use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder; - use Symfony\Component\Security\Core\Exception\BadCredentialsException; - - class FoobarEncoder extends BasePasswordEncoder - { - public function encodePassword($raw, $salt) - { - if ($this->isPasswordTooLong($raw)) { - throw new BadCredentialsException('Invalid password.'); - } - - // ... - } - - public function isPasswordValid($encoded, $raw, $salt) - { - if ($this->isPasswordTooLong($raw)) { - return false; - } - - // ... - } - } - -Using Password Encoders -~~~~~~~~~~~~~~~~~~~~~~~ - -When the :method:`Symfony\\Component\\Security\\Core\\Encoder\\EncoderFactory::getEncoder` -method of the password encoder factory is called with the user object as -its first argument, it will return an encoder of type :class:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface` -which should be used to encode this user's password:: - - // a Acme\Entity\LegacyUser instance - $user = ...; - - // the password that was submitted, e.g. when registering - $plainPassword = ...; - - $encoder = $encoderFactory->getEncoder($user); - - // returns $weakEncoder (see above) - $encodedPassword = $encoder->encodePassword($plainPassword, $user->getSalt()); - - $user->setPassword($encodedPassword); - - // ... save the user - -Now, when you want to check if the submitted password (e.g. when trying to log -in) is correct, you can use:: - - // fetch the Acme\Entity\LegacyUser - $user = ...; - - // the submitted password, e.g. from the login form - $plainPassword = ...; - - $validPassword = $encoder->isPasswordValid( - $user->getPassword(), // the encoded password - $plainPassword, // the submitted password - $user->getSalt() - ); - -Authentication Events ---------------------- - -The security component provides the following authentication events: - -=============================== ======================================================================== ============================================================================== -Name Event Constant Argument Passed to the Listener -=============================== ======================================================================== ============================================================================== -security.authentication.success ``AuthenticationEvents::AUTHENTICATION_SUCCESS`` :class:`Symfony\\Component\\Security\\Core\\Event\\AuthenticationSuccessEvent` -security.authentication.failure ``AuthenticationEvents::AUTHENTICATION_FAILURE`` :class:`Symfony\\Component\\Security\\Core\\Event\\AuthenticationFailureEvent` -security.interactive_login ``SecurityEvents::INTERACTIVE_LOGIN`` :class:`Symfony\\Component\\Security\\Http\\Event\\InteractiveLoginEvent` -security.switch_user ``SecurityEvents::SWITCH_USER`` :class:`Symfony\\Component\\Security\\Http\\Event\\SwitchUserEvent` -security.logout_on_change ``Symfony\Component\Security\Http\Event\DeauthenticatedEvent::class`` :class:`Symfony\\Component\\Security\\Http\\Event\\DeauthenticatedEvent` -=============================== ======================================================================== ============================================================================== - -Authentication Success and Failure Events -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When a provider authenticates the user, a ``security.authentication.success`` -event is dispatched. But beware - this event may fire, for example, on *every* -request if you have session-based authentication, if ``always_authenticate_before_granting`` -is enabled or if the token is not authenticated before AccessListener is invoked. -See ``security.interactive_login`` below if you need to do something when a user *actually* logs in. - -When a provider attempts authentication but fails (i.e. throws an ``AuthenticationException``), -a ``security.authentication.failure`` event is dispatched. You could listen on -the ``security.authentication.failure`` event, for example, in order to log -failed login attempts. - -Security Events -~~~~~~~~~~~~~~~ - -The ``security.interactive_login`` event is triggered after a user has actively -logged into your website. It is important to distinguish this action from -non-interactive authentication methods, such as: - -* authentication based on your session. -* authentication using a HTTP basic header. - -You could listen on the ``security.interactive_login`` event, for example, in -order to give your user a welcome flash message every time they log in. - -The ``security.switch_user`` event is triggered every time you activate -the ``switch_user`` firewall listener. - -The ``Symfony\Component\Security\Http\Event\DeauthenticatedEvent`` event is triggered when a token has been deauthenticated -because of a user change. It can help you perform clean-up tasks. - -.. versionadded:: 4.3 - - The ``Symfony\Component\Security\Http\Event\DeauthenticatedEvent`` event was introduced in Symfony 4.3. - -.. seealso:: - - For more information on switching users, see - :doc:`/security/impersonating_user`. - -.. _`CVE-2013-5750`: https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form diff --git a/components/security/authorization.rst b/components/security/authorization.rst deleted file mode 100644 index dee133d53d5..00000000000 --- a/components/security/authorization.rst +++ /dev/null @@ -1,263 +0,0 @@ -.. index:: - single: Security, Authorization - -Authorization -============= - -When any of the authentication providers (see :ref:`authentication_providers`) -has verified the still-unauthenticated token, an authenticated token will -be returned. The authentication listener should set this token directly -in the :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorageInterface` -using its :method:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorageInterface::setToken` -method. - -From then on, the user is authenticated, i.e. identified. Now, other parts -of the application can use the token to decide whether or not the user may -request a certain URI, or modify a certain object. This decision will be made -by an instance of :class:`Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManagerInterface`. - -An authorization decision will always be based on a few things: - -* The current token - For instance, the token's :method:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface::getRoles` - method may be used to retrieve the roles of the current user (e.g. - ``ROLE_SUPER_ADMIN``), or a decision may be based on the class of the token. -* A set of attributes - Each attribute stands for a certain right the user should have, e.g. - ``ROLE_ADMIN`` to make sure the user is an administrator. -* An object (optional) - Any object for which access control needs to be checked, like - an article or a comment object. - -.. _components-security-access-decision-manager: - -Access Decision Manager ------------------------ - -Since deciding whether or not a user is authorized to perform a certain -action can be a complicated process, the standard :class:`Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager` -itself depends on multiple voters, and makes a final verdict based on all -the votes (either positive, negative or neutral) it has received. It -recognizes several strategies: - -``affirmative`` (default) - grant access as soon as there is one voter granting access; - -``consensus`` - grant access if there are more voters granting access than there are denying; - -``unanimous`` - only grant access if none of the voters has denied access. If all voters - abstained from voting, the decision is based on the ``allow_if_all_abstain`` - config option (which defaults to ``false``). - -Usage of the available options in detail:: - - use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; - - // instances of Symfony\Component\Security\Core\Authorization\Voter\VoterInterface - $voters = [...]; - - // one of "affirmative", "consensus", "unanimous" - $strategy = ...; - - // whether or not to grant access when all voters abstain - $allowIfAllAbstainDecisions = ...; - - // whether or not to grant access when there is no majority (applies only to the "consensus" strategy) - $allowIfEqualGrantedDeniedDecisions = ...; - - $accessDecisionManager = new AccessDecisionManager( - $voters, - $strategy, - $allowIfAllAbstainDecisions, - $allowIfEqualGrantedDeniedDecisions - ); - -.. seealso:: - - You can change the default strategy in the - :ref:`configuration `. - -Voters ------- - -Voters are instances -of :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`, -which means they have to implement a few methods which allows the decision -manager to use them: - -``vote(TokenInterface $token, $subject, array $attributes)`` - this method will do the actual voting and return a value equal to one - of the class constants of :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`, - i.e. ``VoterInterface::ACCESS_GRANTED``, ``VoterInterface::ACCESS_DENIED`` - or ``VoterInterface::ACCESS_ABSTAIN``; - -The Security component contains some standard voters which cover many use -cases: - -AuthenticatedVoter -~~~~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AuthenticatedVoter` -voter supports the attributes ``IS_AUTHENTICATED_FULLY``, ``IS_AUTHENTICATED_REMEMBERED``, -and ``IS_AUTHENTICATED_ANONYMOUSLY`` and grants access based on the current -level of authentication, i.e. is the user fully authenticated, or only based -on a "remember-me" cookie, or even authenticated anonymously?:: - - use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver; - - $trustResolver = new AuthenticationTrustResolver(); - - $authenticatedVoter = new AuthenticatedVoter($trustResolver); - - // instance of Symfony\Component\Security\Core\Authentication\Token\TokenInterface - $token = ...; - - // any object - $object = ...; - - $vote = $authenticatedVoter->vote($token, $object, ['IS_AUTHENTICATED_FULLY']); - -RoleVoter -~~~~~~~~~ - -The :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\RoleVoter` -supports attributes starting with ``ROLE_`` and grants access to the user -when at least one required ``ROLE_*`` attribute can be found in the array of -roles returned by the token's :method:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface::getRoles` -method:: - - use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter; - - $roleVoter = new RoleVoter('ROLE_'); - - $roleVoter->vote($token, $object, ['ROLE_ADMIN']); - -RoleHierarchyVoter -~~~~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\RoleHierarchyVoter` -extends :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\RoleVoter` -and provides some additional functionality: it knows how to handle a -hierarchy of roles. For instance, a ``ROLE_SUPER_ADMIN`` role may have sub-roles -``ROLE_ADMIN`` and ``ROLE_USER``, so that when a certain object requires the -user to have the ``ROLE_ADMIN`` role, it grants access to users who in fact -have the ``ROLE_ADMIN`` role, but also to users having the ``ROLE_SUPER_ADMIN`` -role:: - - use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter; - use Symfony\Component\Security\Core\Role\RoleHierarchy; - - $hierarchy = [ - 'ROLE_SUPER_ADMIN' => ['ROLE_ADMIN', 'ROLE_USER'], - ]; - - $roleHierarchy = new RoleHierarchy($hierarchy); - - $roleHierarchyVoter = new RoleHierarchyVoter($roleHierarchy); - -ExpressionVoter -~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\ExpressionVoter` -grants access based on the evaluation of expressions created with the -:doc:`ExpressionLanguage component `. These -expressions have access to a number of -:ref:`special security variables `:: - - use Symfony\Component\ExpressionLanguage\Expression; - use Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter; - - // Symfony\Component\Security\Core\Authorization\ExpressionLanguage; - $expressionLanguage = ...; - - // instance of Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface - $trustResolver = ...; - - // Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface - $authorizationChecker = ...; - - $expressionVoter = new ExpressionVoter($expressionLanguage, $trustResolver, $authorizationChecker); - - // instance of Symfony\Component\Security\Core\Authentication\Token\TokenInterface - $token = ...; - - // any object - $object = ...; - - $expression = new Expression( - '"ROLE_ADMIN" in roles or (not is_anonymous() and user.isSuperAdmin())' - ); - - $vote = $expressionVoter->vote($token, $object, [$expression]); - -.. note:: - - When you make your own voter, you can use its constructor to inject any - dependencies it needs to come to a decision. - -Roles ------ - -Roles are strings that give expression to a certain right the user has (e.g. -*"edit a blog post"*, *"create an invoice"*). You can freely choose those -strings. The only requirement is that they must start with the ``ROLE_`` prefix -(e.g. ``ROLE_POST_EDIT``, ``ROLE_INVOICE_CREATE``). - -Using the Decision Manager --------------------------- - -The Access Listener -~~~~~~~~~~~~~~~~~~~ - -The access decision manager can be used at any point in a request to decide whether -or not the current user is entitled to access a given resource. One optional, -but useful, method for restricting access based on a URL pattern is the -:class:`Symfony\\Component\\Security\\Http\\Firewall\\AccessListener`, -which is one of the firewall listeners (see :ref:`firewall_listeners`) that -is triggered for each request matching the firewall map (see :ref:`firewall`). - -It uses an access map (which should be an instance of :class:`Symfony\\Component\\Security\\Http\\AccessMapInterface`) -which contains request matchers and a corresponding set of attributes that -are required for the current user to get access to the application:: - - use Symfony\Component\HttpFoundation\RequestMatcher; - use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; - use Symfony\Component\Security\Http\AccessMap; - use Symfony\Component\Security\Http\Firewall\AccessListener; - - $accessMap = new AccessMap(); - $tokenStorage = new TokenStorage(); - $requestMatcher = new RequestMatcher('^/admin'); - $accessMap->add($requestMatcher, ['ROLE_ADMIN']); - - $accessListener = new AccessListener( - $tokenStorage, - $accessDecisionManager, - $accessMap, - $authenticationManager - ); - -Authorization Checker -~~~~~~~~~~~~~~~~~~~~~ - -The access decision manager is also available to other parts of the application -via the :method:`Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationChecker::isGranted` -method of the :class:`Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationChecker`. -A call to this method will directly delegate the question to the access -decision manager:: - - use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; - use Symfony\Component\Security\Core\Exception\AccessDeniedException; - - $authorizationChecker = new AuthorizationChecker( - $tokenStorage, - $authenticationManager, - $accessDecisionManager - ); - - if (!$authorizationChecker->isGranted('ROLE_ADMIN')) { - throw new AccessDeniedException(); - } - diff --git a/components/security/firewall.rst b/components/security/firewall.rst deleted file mode 100644 index adb0fae6e4a..00000000000 --- a/components/security/firewall.rst +++ /dev/null @@ -1,164 +0,0 @@ -.. index:: - single: Security, Firewall - -The Firewall and Authorization -============================== - -Central to the Security component is authorization. This is handled by an instance -of :class:`Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationCheckerInterface`. -When all steps in the process of authenticating the user have been taken successfully, -you can ask the authorization checker if the authenticated user has access to a -certain action or resource of the application:: - - use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; - use Symfony\Component\Security\Core\Exception\AccessDeniedException; - - // instance of Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface - $tokenStorage = ...; - - // instance of Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface - $authenticationManager = ...; - - // instance of Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface - $accessDecisionManager = ...; - - $authorizationChecker = new AuthorizationChecker( - $tokenStorage, - $authenticationManager, - $accessDecisionManager - ); - - // ... authenticate the user - - if (!$authorizationChecker->isGranted('ROLE_ADMIN')) { - throw new AccessDeniedException(); - } - -.. note:: - - Read the dedicated articles to learn more about :doc:`/components/security/authentication` - and :doc:`/components/security/authorization`. - -.. _firewall: - -A Firewall for HTTP Requests ----------------------------- - -Authenticating a user is done by the firewall. An application may have -multiple secured areas, so the firewall is configured using a map of these -secured areas. For each of these areas, the map contains a request matcher -and a collection of listeners. The request matcher gives the firewall the -ability to find out if the current request points to a secured area. -The listeners are then asked if the current request can be used to authenticate -the user:: - - use Symfony\Component\HttpFoundation\RequestMatcher; - use Symfony\Component\Security\Http\Firewall\ExceptionListener; - use Symfony\Component\Security\Http\FirewallMap; - - $firewallMap = new FirewallMap(); - - $requestMatcher = new RequestMatcher('^/secured-area/'); - - // array of callables - $listeners = [...]; - - $exceptionListener = new ExceptionListener(...); - - $firewallMap->add($requestMatcher, $listeners, $exceptionListener); - -The firewall map will be given to the firewall as its first argument, together -with the event dispatcher that is used by the :class:`Symfony\\Component\\HttpKernel\\HttpKernel`:: - - use Symfony\Component\HttpKernel\KernelEvents; - use Symfony\Component\Security\Http\Firewall; - - // the EventDispatcher used by the HttpKernel - $dispatcher = ...; - - $firewall = new Firewall($firewallMap, $dispatcher); - - $dispatcher->addListener( - KernelEvents::REQUEST, - [$firewall, 'onKernelRequest'] - ); - -The firewall is registered to listen to the ``kernel.request`` event that -will be dispatched by the HttpKernel at the beginning of each request -it processes. This way, the firewall may prevent the user from going any -further than allowed. - -Firewall Config -~~~~~~~~~~~~~~~ - -The information about a given firewall, such as its name, provider, context, -entry point and access denied URL, is provided by instances of the -:class:`Symfony\\Bundle\\SecurityBundle\\Security\\FirewallConfig` class. - -This object can be accessed through the ``getFirewallConfig(Request $request)`` -method of the :class:`Symfony\\Bundle\\SecurityBundle\\Security\\FirewallMap` class and -through the ``getConfig()`` method of the -:class:`Symfony\\Bundle\\SecurityBundle\\Security\\FirewallContext` class. - -.. _firewall_listeners: - -Firewall Listeners -~~~~~~~~~~~~~~~~~~ - -When the firewall gets notified of the ``kernel.request`` event, it asks -the firewall map if the request matches one of the secured areas. The first -secured area that matches the request will return a set of corresponding -firewall listeners (which each is a callable). -These listeners will all be asked to handle the current request. This basically -means: find out if the current request contains any information by which -the user might be authenticated (for instance the Basic HTTP authentication -listener checks if the request has a header called ``PHP_AUTH_USER``). - -Exception Listener -~~~~~~~~~~~~~~~~~~ - -If any of the listeners throws an :class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException`, -the exception listener that was provided when adding secured areas to the -firewall map will jump in. - -The exception listener determines what happens next, based on the arguments -it received when it was created. It may start the authentication procedure, -perhaps ask the user to supply their credentials again (when they have only been -authenticated based on a "remember-me" cookie), or transform the exception -into an :class:`Symfony\\Component\\HttpKernel\\Exception\\AccessDeniedHttpException`, -which will eventually result in an "HTTP/1.1 403: Access Denied" response. - -Entry Points -~~~~~~~~~~~~ - -When the user is not authenticated at all (i.e. when the token storage -has no token yet), the firewall's entry point will be called to "start" -the authentication process. An entry point should implement -:class:`Symfony\\Component\\Security\\Http\\EntryPoint\\AuthenticationEntryPointInterface`, -which has only one method: :method:`Symfony\\Component\\Security\\Http\\EntryPoint\\AuthenticationEntryPointInterface::start`. -This method receives the current :class:`Symfony\\Component\\HttpFoundation\\Request` -object and the exception by which the exception listener was triggered. -The method should return a :class:`Symfony\\Component\\HttpFoundation\\Response` -object. This could be, for instance, the page containing the login form or, -in the case of Basic HTTP authentication, a response with a ``WWW-Authenticate`` -header, which will prompt the user to supply their username and password. - -Flow: Firewall, Authentication, Authorization ---------------------------------------------- - -Hopefully you can now see a little bit about how the "flow" of the security -context works: - -#. The Firewall is registered as a listener on the ``kernel.request`` event; -#. At the beginning of the request, the Firewall checks the firewall map - to see if any firewall should be active for this URL; -#. If a firewall is found in the map for this URL, its listeners are notified; -#. Each listener checks to see if the current request contains any authentication - information - a listener may (a) authenticate a user, (b) throw an - ``AuthenticationException``, or (c) do nothing (because there is no - authentication information on the request); -#. Once a user is authenticated, you'll use :doc:`/components/security/authorization` - to deny access to certain resources. - -Read the next articles to find out more about :doc:`/components/security/authentication` -and :doc:`/components/security/authorization`. diff --git a/components/security/secure_tools.rst b/components/security/secure_tools.rst deleted file mode 100644 index a9d6e0fec3a..00000000000 --- a/components/security/secure_tools.rst +++ /dev/null @@ -1,56 +0,0 @@ -Securely Generating Random Values -================================= - -The Symfony Security component comes with a collection of nice utilities -related to security. These utilities are used by Symfony, but you should -also use them if you want to solve the problem they address. - -.. note:: - - The functions described in this article were introduced in PHP 5.6 or 7. - For older PHP versions, a polyfill is provided by the - `Symfony Polyfill Component`_. - -Comparing Strings -~~~~~~~~~~~~~~~~~ - -The time it takes to compare two strings depends on their differences. This -can be used by an attacker when the two strings represent a password for -instance; it is known as a `Timing attack`_. - -When comparing two passwords, you should use the :phpfunction:`hash_equals` -function:: - - if (hash_equals($knownString, $userInput)) { - // ... - } - -Generating a Secure Random String -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Whenever you need to generate a secure random string, you are highly -encouraged to use the :phpfunction:`random_bytes` function:: - - $random = random_bytes(10); - -The function returns a random string, suitable for cryptographic use, of -the number bytes passed as an argument (10 in the above example). - -.. tip:: - - The ``random_bytes()`` function returns a binary string which may contain - the ``\0`` character. This can cause trouble in several common scenarios, - such as storing this value in a database or including it as part of the - URL. The solution is to hash the value returned by ``random_bytes()`` with - a hashing function such as :phpfunction:`md5` or :phpfunction:`sha1`. - -Generating a Secure Random Number -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you need to generate a cryptographically secure random integer, you should -use the :phpfunction:`random_int` function:: - - $random = random_int(1, 10); - -.. _`Timing attack`: https://en.wikipedia.org/wiki/Timing_attack -.. _`Symfony Polyfill Component`: https://github.com/symfony/polyfill diff --git a/components/semaphore.rst b/components/semaphore.rst new file mode 100644 index 00000000000..81bf2b6fe39 --- /dev/null +++ b/components/semaphore.rst @@ -0,0 +1,77 @@ +.. index:: + single: Semaphore + single: Components; Semaphore + +The Semaphore Component +======================= + + The Semaphore Component manages `semaphores`_, a mechanism to provide + exclusive access to a shared resource. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/semaphore + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +In computer science, a semaphore is a variable or abstract data type used to +control access to a common resource by multiple processes in a concurrent +system such as a multitasking operating system. The main difference +with :doc:`locks ` is that semaphores allow more than one process to +access a resource, whereas locks only allow one process. + +Create semaphores with the :class:`Symfony\\Component\\Semaphore\\SemaphoreFactory` +class, which in turn requires another class to manage the storage:: + + use Symfony\Component\Semaphore\SemaphoreFactory; + use Symfony\Component\Semaphore\Store\RedisStore; + + $redis = new Redis(); + $redis->connect('172.17.0.2'); + + $store = new RedisStore($redis); + $factory = new SemaphoreFactory($store); + +The semaphore is created by calling the +:method:`Symfony\\Component\\Semaphore\\SemaphoreFactory::createSemaphore` +method. Its first argument is an arbitrary string that represents the locked +resource. Its second argument is the maximum number of processes allowed. Then, a +call to the :method:`Symfony\\Component\\Semaphore\\SemaphoreInterface::acquire` +method will try to acquire the semaphore:: + + // ... + $semaphore = $factory->createSemaphore('pdf-invoice-generation', 2); + + if ($semaphore->acquire()) { + // The resource "pdf-invoice-generation" is locked. + // Here you can safely compute and generate the invoice. + + $semaphore->release(); + } + +If the semaphore can not be acquired, the method returns ``false``. The +``acquire()`` method can be safely called repeatedly, even if the semaphore is +already acquired. + +.. note:: + + Unlike other implementations, the Semaphore component distinguishes + semaphores instances even when they are created for the same resource. If a + semaphore has to be used by several services, they should share the same + ``Semaphore`` instance returned by the ``SemaphoreFactory::createSemaphore`` + method. + +.. tip:: + + If you don't release the semaphore explicitly, it will be released + automatically on instance destruction. In some cases, it can be useful to + lock a resource across several requests. To disable the automatic release + behavior, set the fifth argument of the ``createSemaphore()`` method to ``false``. + +.. _`semaphores`: https://en.wikipedia.org/wiki/Semaphore_(programming) diff --git a/components/serializer.rst b/components/serializer.rst index 7b5c7af5ca0..ddefeced17d 100644 --- a/components/serializer.rst +++ b/components/serializer.rst @@ -229,10 +229,10 @@ normalized data, instead of the denormalizer re-creating them. Note that arrays of objects. Those will still be replaced when present in the normalized data. -.. versionadded:: 4.3 +Context +------- - The ``AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE`` option was - introduced in Symfony 4.3. +Many Serializer features can be configured :doc:`using a context `. .. _component-serializer-attributes-groups: @@ -298,7 +298,7 @@ Then, create your groups definition: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes namespace Acme; @@ -306,14 +306,13 @@ Then, create your groups definition: class MyObj { - /** - * @Groups({"group1", "group2"}) - */ + #[Groups(['group1', 'group2'])] public $foo; - /** - * @Groups("group3") - */ + #[Groups(['group4'])] + public $anotherProperty; + + #[Groups(['group3'])] public function getBar() // is* methods are also supported { return $this->bar; @@ -328,6 +327,8 @@ Then, create your groups definition: attributes: foo: groups: ['group1', 'group2'] + anotherProperty: + groups: ['group4'] bar: groups: ['group3'] @@ -345,6 +346,10 @@ Then, create your groups definition: group2 + + group4 + + group3 @@ -358,6 +363,7 @@ You are now able to serialize only attributes in the groups you want:: $obj = new MyObj(); $obj->foo = 'foo'; + $obj->anotherProperty = 'anotherProperty'; $obj->setBar('bar'); $normalizer = new ObjectNormalizer($classMetadataFactory); @@ -367,13 +373,22 @@ You are now able to serialize only attributes in the groups you want:: // $data = ['foo' => 'foo']; $obj2 = $serializer->denormalize( - ['foo' => 'foo', 'bar' => 'bar'], + ['foo' => 'foo', 'anotherProperty' => 'anotherProperty', 'bar' => 'bar'], 'MyObj', null, ['groups' => ['group1', 'group3']] ); // $obj2 = MyObj(foo: 'foo', bar: 'bar') + // To get all groups, use the special value `*` in `groups` + $obj3 = $serializer->denormalize( + ['foo' => 'foo', 'anotherProperty' => 'anotherProperty', 'bar' => 'bar'], + 'MyObj', + null, + ['groups' => ['*']] + ); + // $obj2 = MyObj(foo: 'foo', anotherProperty: 'anotherProperty', bar: 'bar') + .. _ignoring-attributes-when-serializing: Selecting Specific Attributes @@ -420,9 +435,70 @@ As for groups, attributes can be selected during both the serialization and dese Ignoring Attributes ------------------- -As an option, there's a way to ignore attributes from the origin object. -To remove those attributes provide an array via the ``AbstractNormalizer::IGNORED_ATTRIBUTES`` -key in the ``context`` parameter of the desired serializer method:: +All attributes are included by default when serializing objects. There are two +options to ignore some of those attributes. + +Option 1: Using ``@Ignore`` Annotation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. configuration-block:: + + .. code-block:: php-attributes + + namespace App\Model; + + use Symfony\Component\Serializer\Annotation\Ignore; + + class MyClass + { + public $foo; + + #[Ignore] + public $bar; + } + + .. code-block:: yaml + + App\Model\MyClass: + attributes: + bar: + ignore: true + + .. code-block:: xml + + + + + + + + +You can now ignore specific attributes during serialization:: + + use App\Model\MyClass; + use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + use Symfony\Component\Serializer\Serializer; + + $obj = new MyClass(); + $obj->foo = 'foo'; + $obj->bar = 'bar'; + + $normalizer = new ObjectNormalizer($classMetadataFactory); + $serializer = new Serializer([$normalizer]); + + $data = $serializer->normalize($obj); + // $data = ['foo' => 'foo']; + +Option 2: Using the Context +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Pass an array with the names of the attributes to ignore using the +``AbstractNormalizer::IGNORED_ATTRIBUTES`` key in the ``context`` of the +serializer method:: use Acme\Person; use Symfony\Component\Serializer\Encoder\JsonEncoder; @@ -440,12 +516,6 @@ key in the ``context`` parameter of the desired serializer method:: $serializer = new Serializer([$normalizer], [$encoder]); $serializer->serialize($person, 'json', [AbstractNormalizer::IGNORED_ATTRIBUTES => ['age']]); // Output: {"name":"foo"} -.. deprecated:: 4.2 - - The :method:`Symfony\\Component\\Serializer\\Normalizer\\AbstractNormalizer::setIgnoredAttributes` - method that was used as an alternative to the ``AbstractNormalizer::IGNORED_ATTRIBUTES`` option - was deprecated in Symfony 4.2. - .. _component-serializer-converting-property-names-when-serializing-and-deserializing: Converting Property Names when Serializing and Deserializing @@ -476,12 +546,12 @@ A custom name converter can handle such cases:: class OrgPrefixNameConverter implements NameConverterInterface { - public function normalize($propertyName) + public function normalize(string $propertyName): string { return 'org_'.$propertyName; } - public function denormalize($propertyName) + public function denormalize(string $propertyName): string { // removes 'org_' prefix return 'org_' === substr($propertyName, 0, 4) ? substr($propertyName, 4) : $propertyName; @@ -585,7 +655,7 @@ defines a ``Person`` entity with a ``firstName`` property: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes namespace App\Entity; @@ -593,9 +663,7 @@ defines a ``Person`` entity with a ``firstName`` property: class Person { - /** - * @SerializedName("customer_name") - */ + #[SerializedName('customer_name')] private $firstName; public function __construct($firstName) @@ -639,17 +707,15 @@ If you are using isser methods (methods prefixed by ``is``, like ``App\Model\Person::isSportsperson()``), the Serializer component will automatically detect and use it to serialize related attributes. -The ``ObjectNormalizer`` also takes care of methods starting with ``has`` and -``get``. +The ``ObjectNormalizer`` also takes care of methods starting with ``has``, ``get``, +and ``can``. -Using Callbacks to Serialize Properties with Object Instances -------------------------------------------------------------- +.. versionadded:: 6.1 -.. deprecated:: 4.2 + The support of canners (methods prefixed by ``can``) was introduced in Symfony 6.1. - The :method:`Symfony\\Component\\Serializer\\Normalizer\\AbstractNormalizer::setCallbacks` - method is deprecated since Symfony 4.2. Use the ``callbacks`` - key of the context instead. +Using Callbacks to Serialize Properties with Object Instances +------------------------------------------------------------- When serializing, you can set a callback to format a specific object property:: @@ -683,11 +749,6 @@ When serializing, you can set a callback to format a specific object property:: $serializer->serialize($person, 'json'); // Output: {"name":"cordoval", "age": 34, "createdAt": "2014-03-22T09:43:12-0500"} -.. deprecated:: 4.2 - - The :method:`Symfony\\Component\\Serializer\\Normalizer\\AbstractNormalizer::setCallbacks` is deprecated since - Symfony 4.2, use the "callbacks" key of the context instead. - .. _component-serializer-normalizers: Normalizers @@ -715,12 +776,12 @@ The Serializer component provides several built-in normalizers: :class:`Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer` This normalizer leverages the :doc:`PropertyAccess Component ` to read and write in the object. It means that it can access to properties - directly and through getters, setters, hassers, issers, adders and removers. It supports - calling the constructor during the denormalization process. + directly and through getters, setters, hassers, issers, canners, adders and removers. + It supports calling the constructor during the denormalization process. Objects are normalized to a map of property names and values (names are - generated by removing the ``get``, ``set``, ``has``, ``is``, ``add`` or ``remove`` prefix from - the method name and transforming the first letter to lowercase; e.g. + generated by removing the ``get``, ``set``, ``has``, ``is``, ``can``, ``add`` or ``remove`` + prefix from the method name and transforming the first letter to lowercase; e.g. ``getFirstName()`` -> ``firstName``). The ``ObjectNormalizer`` is the most powerful normalizer. It is configured by @@ -743,6 +804,16 @@ The Serializer component provides several built-in normalizers: Objects are normalized to a map of property names to property values. + If you prefer to only normalize certain properties (e.g. only public properties) + set the ``PropertyNormalizer::NORMALIZE_VISIBILITY`` context option and + combine the following values: ``PropertyNormalizer::NORMALIZE_PUBLIC``, + ``PropertyNormalizer::NORMALIZE_PROTECTED`` or ``PropertyNormalizer::NORMALIZE_PRIVATE``. + + .. versionadded:: 6.2 + + The ``PropertyNormalizer::NORMALIZE_VISIBILITY`` context option and its + values were introduced in Symfony 6.2. + :class:`Symfony\\Component\\Serializer\\Normalizer\\JsonSerializableNormalizer` This normalizer works with classes that implement :phpclass:`JsonSerializable`. @@ -765,10 +836,6 @@ The Serializer component provides several built-in normalizers: This normalizer converts :phpclass:`DateTimeZone` objects into strings that represent the name of the timezone according to the `list of PHP timezones`_. - .. versionadded:: 4.3 - - The ``DateTimeZoneNormalizer`` was introduced in Symfony 4.3. - :class:`Symfony\\Component\\Serializer\\Normalizer\\DataUriNormalizer` This normalizer converts :phpclass:`SplFileInfo` objects into a `data URI`_ string (``data:...``) such that files can be embedded into serialized data. @@ -777,25 +844,40 @@ The Serializer component provides several built-in normalizers: This normalizer converts :phpclass:`DateInterval` objects into strings. By default, it uses the ``P%yY%mM%dDT%hH%iM%sS`` format. +:class:`Symfony\\Component\\Serializer\\Normalizer\\BackedEnumNormalizer` + This normalizer converts a \BackedEnum objects into strings or integers. + +:class:`Symfony\\Component\\Serializer\\Normalizer\\FormErrorNormalizer` + This normalizer works with classes that implement + :class:`Symfony\\Component\\Form\\FormInterface`. + + It will get errors from the form and normalize them into a normalized array. + :class:`Symfony\\Component\\Serializer\\Normalizer\\ConstraintViolationListNormalizer` This normalizer converts objects that implement :class:`Symfony\\Component\\Validator\\ConstraintViolationListInterface` into a list of errors according to the `RFC 7807`_ standard. - .. versionadded:: 4.1 - - The ``ConstraintViolationListNormalizer`` was introduced in Symfony 4.1. - :class:`Symfony\\Component\\Serializer\\Normalizer\\ProblemNormalizer` Normalizes errors according to the API Problem spec `RFC 7807`_. - .. versionadded:: 4.4 - - The ``ProblemNormalizer`` was introduced in Symfony 4.4. - :class:`Symfony\\Component\\Serializer\\Normalizer\\CustomNormalizer` Normalizes a PHP object using an object that implements :class:`Symfony\\Component\\Serializer\\Normalizer\\NormalizableInterface`. +:class:`Symfony\\Component\\Serializer\\Normalizer\\UidNormalizer` + This normalizer converts objects that implement + :class:`Symfony\\Component\\Uid\\AbstractUid` into strings. + The default normalization format for objects that implement :class:`Symfony\\Component\\Uid\\Uuid` + is the `RFC 4122`_ format (example: ``d9e7a184-5d5b-11ea-a62a-3499710062d0``). + The default normalization format for objects that implement :class:`Symfony\\Component\\Uid\\Ulid` + is the Base 32 format (example: ``01E439TP9XJZ9RPFH3T1PYBCR8``). + You can change the string format by setting the serializer context option + ``UidNormalizer::NORMALIZATION_FORMAT_KEY`` to ``UidNormalizer::NORMALIZATION_FORMAT_BASE_58``, + ``UidNormalizer::NORMALIZATION_FORMAT_BASE_32`` or ``UidNormalizer::NORMALIZATION_FORMAT_RFC_4122``. + + Also it can denormalize ``uuid`` or ``ulid`` strings to :class:`Symfony\\Component\\Uid\\Uuid` + or :class:`Symfony\\Component\\Uid\\Ulid`. The format does not matter. + .. note:: You can also create your own Normalizer to use another structure. Read more at @@ -934,6 +1016,8 @@ Option Description D ``csv_delimiter`` Sets the field delimiter separating values (one ``,`` character only) ``csv_enclosure`` Sets the field enclosure (one character only) ``"`` +``csv_end_of_line`` Sets the character(s) used to mark the end of each ``\n`` + line in the CSV file ``csv_escape_char`` Sets the escape character (at most one character) empty string ``csv_key_separator`` Sets the separator for array's keys during its ``.`` flattening @@ -942,7 +1026,7 @@ Option Description D and ``$options = ['csv_headers' => ['a', 'b', 'c']]`` then ``serialize($data, 'csv', $options)`` returns ``a,b,c\n1,2,3`` ``[]``, inferred from input data's keys -``csv_escape_formulas`` Escapes fields containing formulas by prepending them ``false`` +``csv_escape_formulas`` Escapes fields containing formulas by prepending them ``false`` with a ``\t`` character ``as_collection`` Always returns results as a collection, even if only ``true`` one line is decoded. @@ -950,10 +1034,6 @@ Option Description D ``output_utf8_bom`` Outputs special `UTF-8 BOM`_ along with encoded data ``false`` ======================= ===================================================== ========================== -.. versionadded:: 4.4 - - The ``output_utf8_bom`` option was introduced in Symfony 4.4. - The ``XmlEncoder`` ~~~~~~~~~~~~~~~~~~ @@ -1007,8 +1087,7 @@ always as a collection. .. tip:: XML comments are ignored by default when decoding contents, but this - behavior can be changed with the optional ``$decoderIgnoredNodeTypes`` argument of - the ``XmlEncoder`` class constructor. + behavior can be changed with the optional context key ``XmlEncoder::DECODER_IGNORED_NODE_TYPES``. Data with ``#comment`` keys are encoded to XML comments by default. This can be changed with the optional ``$encoderIgnoredNodeTypes`` argument of the @@ -1046,11 +1125,6 @@ Option Description generated XML ============================== ================================================= ========================== -.. versionadded:: 4.2 - - The ``decoder_ignored_node_types`` and ``encoder_ignored_node_types`` - options were introduced in Symfony 4.2. - Example with custom ``context``:: use Symfony\Component\Serializer\Encoder\XmlEncoder; @@ -1110,6 +1184,44 @@ Option Description Defaul to customize the encoding / decoding YAML string =============== ======================================================== ========================== +.. _component-serializer-context-builders: + +Context Builders +---------------- + +Instead of passing plain PHP arrays to the :ref:`serialization context `, +you can use "context builders" to define the context using a fluent interface:: + + use Symfony\Component\Serializer\Context\Encoder\CsvEncoderContextBuilder; + use Symfony\Component\Serializer\Context\Normalizer\ObjectNormalizerContextBuilder; + + $initialContext = [ + 'custom_key' => 'custom_value', + ]; + + $contextBuilder = (new ObjectNormalizerContextBuilder()) + ->withContext($initialContext) + ->withGroups(['group1', 'group2']); + + $contextBuilder = (new CsvEncoderContextBuilder()) + ->withContext($contextBuilder) + ->withDelimiter(';'); + + $serializer->serialize($something, 'csv', $contextBuilder->toArray()); + +.. versionadded:: 6.1 + + Context builders were introduced in Symfony 6.1. + +.. note:: + + The Serializer component provides a context builder + for each :ref:`normalizer ` + and :ref:`encoder `. + + You can also :doc:`create custom context builders ` + to deal with your context values. + Skipping ``null`` Values ------------------------ @@ -1128,6 +1240,35 @@ to ``true``:: .. _component-serializer-handling-circular-references: +Collecting Type Errors While Denormalizing +------------------------------------------ + +When denormalizing a payload to an object with typed properties, you'll get an +exception if the payload contains properties that don't have the same type as +the object. + +In those situations, use the ``COLLECT_DENORMALIZATION_ERRORS`` option to +collect all exceptions at once, and to get the object partially denormalized:: + + try { + $dto = $serializer->deserialize($request->getContent(), MyDto::class, 'json', [ + DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, + ]); + } catch (PartialDenormalizationException $e) { + $violations = new ConstraintViolationList(); + /** @var NotNormalizableValueException $exception */ + foreach ($e->getErrors() as $exception) { + $message = sprintf('The type must be one of "%s" ("%s" given).', implode(', ', $exception->getExpectedTypes()), $exception->getCurrentType()); + $parameters = []; + if ($exception->canUseMessageForUser()) { + $parameters['hint'] = $exception->getMessage(); + } + $violations->add(new ConstraintViolation($message, '', $parameters, null, $exception->getPath(), null)); + } + + return $this->json($violations, 400); + } + Handling Circular References ---------------------------- @@ -1221,12 +1362,6 @@ having unique identifiers:: var_dump($serializer->serialize($org, 'json')); // {"name":"Les-Tilleuls.coop","members":[{"name":"K\u00e9vin", organization: "Les-Tilleuls.coop"}]} -.. deprecated:: 4.2 - - The :method:`Symfony\\Component\\Serializer\\Normalizer\\AbstractNormalizer::setCircularReferenceHandler` - method is deprecated since Symfony 4.2. Use the ``AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER`` - key of the context instead. - Handling Serialization Depth ---------------------------- @@ -1262,7 +1397,7 @@ Here, we set it to 2 for the ``$child`` property: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes namespace Acme; @@ -1270,9 +1405,7 @@ Here, we set it to 2 for the ``$child`` property: class MyObj { - /** - * @MaxDepth(2) - */ + #[MaxDepth(2)] public $child; // ... @@ -1337,9 +1470,7 @@ having unique identifiers:: { public $id; - /** - * @MaxDepth(1) - */ + #[MaxDepth(1)] public $child; } @@ -1379,12 +1510,6 @@ having unique identifiers:: ]; */ -.. deprecated:: 4.2 - - The :method:`Symfony\\Component\\Serializer\\Normalizer\\AbstractNormalizer::setMaxDepthHandler` - method is deprecated since Symfony 4.2. Use the ``max_depth_handler`` - key of the context instead. - Handling Arrays --------------- @@ -1577,18 +1702,18 @@ and ``BitBucketCodeRepository`` classes: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes namespace App; + use App\BitBucketCodeRepository; + use App\GitHubCodeRepository; use Symfony\Component\Serializer\Annotation\DiscriminatorMap; - /** - * @DiscriminatorMap(typeProperty="type", mapping={ - * "github"="App\GitHubCodeRepository", - * "bitbucket"="App\BitBucketCodeRepository" - * }) - */ + #[DiscriminatorMap(typeProperty: 'type', mapping: [ + 'github' => GitHubCodeRepository::class, + 'bitbucket' => BitBucketCodeRepository::class, + ])] abstract class CodeRepository { // ... @@ -1661,5 +1786,6 @@ Learn more .. _`Value Objects`: https://en.wikipedia.org/wiki/Value_object .. _`API Platform`: https://api-platform.com .. _`list of PHP timezones`: https://www.php.net/manual/en/timezones.php +.. _`RFC 4122`: https://tools.ietf.org/html/rfc4122 .. _`PHP reflection`: https://php.net/manual/en/book.reflection.php .. _`data URI`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs diff --git a/components/string.rst b/components/string.rst new file mode 100644 index 00000000000..29a74136f63 --- /dev/null +++ b/components/string.rst @@ -0,0 +1,593 @@ +.. index:: + single: String + single: Components; String + +The String Component +==================== + + The String component provides a single object-oriented API to work with + three "unit systems" of strings: bytes, code points and grapheme clusters. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/string + +.. include:: /components/require_autoload.rst.inc + +What is a String? +----------------- + +You can skip this section if you already know what a *"code point"* or a +*"grapheme cluster"* are in the context of handling strings. Otherwise, read +this section to learn about the terminology used by this component. + +Languages like English require a very limited set of characters and symbols to +display any content. Each string is a series of characters (letters or symbols) +and they can be encoded even with the most limited standards (e.g. `ASCII`_). + +However, other languages require thousands of symbols to display their contents. +They need complex encoding standards such as `Unicode`_ and concepts like +"character" no longer make sense. Instead, you have to deal with these terms: + +* `Code points`_: they are the atomic units of information. A string is a series + of code points. Each code point is a number whose meaning is given by the + `Unicode`_ standard. For example, the English letter ``A`` is the ``U+0041`` + code point and the Japanese *kana* ``の`` is the ``U+306E`` code point. +* `Grapheme clusters`_: they are a sequence of one or more code points which are + displayed as a single graphical unit. For example, the Spanish letter ``ñ`` is + a grapheme cluster that contains two code points: ``U+006E`` = ``n`` (*"latin + small letter N"*) + ``U+0303`` = ``◌̃`` (*"combining tilde"*). +* Bytes: they are the actual information stored for the string contents. Each + code point can require one or more bytes of storage depending on the standard + being used (UTF-8, UTF-16, etc.). + +The following image displays the bytes, code points and grapheme clusters for +the same word written in English (``hello``) and Hindi (``नमस्ते``): + +.. image:: /_images/components/string/bytes-points-graphemes.png + :align: center + +Usage +----- + +Create a new object of type :class:`Symfony\\Component\\String\\ByteString`, +:class:`Symfony\\Component\\String\\CodePointString` or +:class:`Symfony\\Component\\String\\UnicodeString`, pass the string contents as +their arguments and then use the object-oriented API to work with those strings:: + + use Symfony\Component\String\UnicodeString; + + $text = (new UnicodeString('This is a déjà-vu situation.')) + ->trimEnd('.') + ->replace('déjà-vu', 'jamais-vu') + ->append('!'); + // $text = 'This is a jamais-vu situation!' + + $content = new UnicodeString('नमस्ते दुनिया'); + if ($content->ignoreCase()->startsWith('नमस्ते')) { + // ... + } + +Method Reference +---------------- + +Methods to Create String Objects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First, you can create objects prepared to store strings as bytes, code points +and grapheme clusters with the following classes:: + + use Symfony\Component\String\ByteString; + use Symfony\Component\String\CodePointString; + use Symfony\Component\String\UnicodeString; + + $foo = new ByteString('hello'); + $bar = new CodePointString('hello'); + // UnicodeString is the most commonly used class + $baz = new UnicodeString('hello'); + +Use the ``wrap()`` static method to instantiate more than one string object:: + + $contents = ByteString::wrap(['hello', 'world']); // $contents = ByteString[] + $contents = UnicodeString::wrap(['I', '❤️', 'Symfony']); // $contents = UnicodeString[] + + // use the unwrap method to make the inverse conversion + $contents = UnicodeString::unwrap([ + new UnicodeString('hello'), new UnicodeString('world'), + ]); // $contents = ['hello', 'world'] + +If you work with lots of String objects, consider using the shortcut functions +to make your code more concise:: + + // the b() function creates byte strings + use function Symfony\Component\String\b; + + // both lines are equivalent + $foo = new ByteString('hello'); + $foo = b('hello'); + + // the u() function creates Unicode strings + use function Symfony\Component\String\u; + + // both lines are equivalent + $foo = new UnicodeString('hello'); + $foo = u('hello'); + + // the s() function creates a byte string or Unicode string + // depending on the given contents + use function Symfony\Component\String\s; + + // creates a ByteString object + $foo = s("\xfe\xff"); + // creates a UnicodeString object + $foo = s('अनुच्छेद'); + +There are also some specialized constructors:: + + // ByteString can create a random string of the given length + $foo = ByteString::fromRandom(12); + // by default, random strings use A-Za-z0-9 characters; you can restrict + // the characters to use with the second optional argument + $foo = ByteString::fromRandom(6, 'AEIOU0123456789'); + $foo = ByteString::fromRandom(10, 'qwertyuiop'); + + // CodePointString and UnicodeString can create a string from code points + $foo = UnicodeString::fromCodePoints(0x928, 0x92E, 0x938, 0x94D, 0x924, 0x947); + // equivalent to: $foo = new UnicodeString('नमस्ते'); + +Methods to Transform String Objects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each string object can be transformed into the other two types of objects:: + + $foo = ByteString::fromRandom(12)->toCodePointString(); + $foo = (new CodePointString('hello'))->toUnicodeString(); + $foo = UnicodeString::fromCodePoints(0x68, 0x65, 0x6C, 0x6C, 0x6F)->toByteString(); + + // the optional $toEncoding argument defines the encoding of the target string + $foo = (new CodePointString('hello'))->toByteString('Windows-1252'); + // the optional $fromEncoding argument defines the encoding of the original string + $foo = (new ByteString('さよなら'))->toCodePointString('ISO-2022-JP'); + +If the conversion is not possible for any reason, you'll get an +:class:`Symfony\\Component\\String\\Exception\\InvalidArgumentException`. + +There is also a method to get the bytes stored at some position:: + + // ('नमस्ते' bytes = [224, 164, 168, 224, 164, 174, 224, 164, 184, + // 224, 165, 141, 224, 164, 164, 224, 165, 135]) + b('नमस्ते')->bytesAt(0); // [224] + u('नमस्ते')->bytesAt(0); // [224, 164, 168] + + b('नमस्ते')->bytesAt(1); // [164] + u('नमस्ते')->bytesAt(1); // [224, 164, 174] + +Methods Related to Length and White Spaces +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + // returns the number of graphemes, code points or bytes of the given string + $word = 'नमस्ते'; + (new ByteString($word))->length(); // 18 (bytes) + (new CodePointString($word))->length(); // 6 (code points) + (new UnicodeString($word))->length(); // 4 (graphemes) + + // some symbols require double the width of others to represent them when using + // a monospaced font (e.g. in a console). This method returns the total width + // needed to represent the entire word + $word = 'नमस्ते'; + (new ByteString($word))->width(); // 18 + (new CodePointString($word))->width(); // 4 + (new UnicodeString($word))->width(); // 4 + // if the text contains multiple lines, it returns the max width of all lines + $text = "<<width(); // 14 + + // only returns TRUE if the string is exactly an empty string (not even white spaces) + u('hello world')->isEmpty(); // false + u(' ')->isEmpty(); // false + u('')->isEmpty(); // true + + // removes all white spaces from the start and end of the string and replaces two + // or more consecutive white spaces inside contents by a single white space + u(" \n\n hello world \n \n")->collapseWhitespace(); // 'hello world' + +Methods to Change Case +~~~~~~~~~~~~~~~~~~~~~~ + +:: + + // changes all graphemes/code points to lower case + u('FOO Bar')->lower(); // 'foo bar' + + // when dealing with different languages, uppercase/lowercase is not enough + // there are three cases (lower, upper, title), some characters have no case, + // case is context-sensitive and locale-sensitive, etc. + // this method returns a string that you can use in case-insensitive comparisons + u('FOO Bar')->folded(); // 'foo bar' + u('Die O\'Brian Straße')->folded(); // "die o'brian strasse" + + // changes all graphemes/code points to upper case + u('foo BAR')->upper(); // 'FOO BAR' + + // changes all graphemes/code points to "title case" + u('foo bar')->title(); // 'Foo bar' + u('foo bar')->title(true); // 'Foo Bar' + + // changes all graphemes/code points to camelCase + u('Foo: Bar-baz.')->camel(); // 'fooBarBaz' + // changes all graphemes/code points to snake_case + u('Foo: Bar-baz.')->snake(); // 'foo_bar_baz' + // other cases can be achieved by chaining methods. E.g. PascalCase: + u('Foo: Bar-baz.')->camel()->title(); // 'FooBarBaz' + +The methods of all string classes are case-sensitive by default. You can perform +case-insensitive operations with the ``ignoreCase()`` method:: + + u('abc')->indexOf('B'); // null + u('abc')->ignoreCase()->indexOf('B'); // 1 + +Methods to Append and Prepend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + // adds the given content (one or more strings) at the beginning/end of the string + u('world')->prepend('hello'); // 'helloworld' + u('world')->prepend('hello', ' '); // 'hello world' + + u('hello')->append('world'); // 'helloworld' + u('hello')->append(' ', 'world'); // 'hello world' + + // adds the given content at the beginning of the string (or removes it) to + // make sure that the content starts exactly with that content + u('Name')->ensureStart('get'); // 'getName' + u('getName')->ensureStart('get'); // 'getName' + u('getgetName')->ensureStart('get'); // 'getName' + // this method is similar, but works on the end of the content instead of on the beginning + u('User')->ensureEnd('Controller'); // 'UserController' + u('UserController')->ensureEnd('Controller'); // 'UserController' + u('UserControllerController')->ensureEnd('Controller'); // 'UserController' + + // returns the contents found before/after the first occurrence of the given string + u('hello world')->before('world'); // 'hello ' + u('hello world')->before('o'); // 'hell' + u('hello world')->before('o', true); // 'hello' + + u('hello world')->after('hello'); // ' world' + u('hello world')->after('o'); // ' world' + u('hello world')->after('o', true); // 'o world' + + // returns the contents found before/after the last occurrence of the given string + u('hello world')->beforeLast('o'); // 'hello w' + u('hello world')->beforeLast('o', true); // 'hello wo' + + u('hello world')->afterLast('o'); // 'rld' + u('hello world')->afterLast('o', true); // 'orld' + +Methods to Pad and Trim +~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + // makes a string as long as the first argument by adding the given + // string at the beginning, end or both sides of the string + u(' Lorem Ipsum ')->padBoth(20, '-'); // '--- Lorem Ipsum ----' + u(' Lorem Ipsum')->padStart(20, '-'); // '-------- Lorem Ipsum' + u('Lorem Ipsum ')->padEnd(20, '-'); // 'Lorem Ipsum --------' + + // repeats the given string the number of times passed as argument + u('_.')->repeat(10); // '_._._._._._._._._._.' + + // removes the given characters (by default, white spaces) from the string + u(' Lorem Ipsum ')->trim(); // 'Lorem Ipsum' + u('Lorem Ipsum ')->trim('m'); // 'Lorem Ipsum ' + u('Lorem Ipsum')->trim('m'); // 'Lorem Ipsu' + + u(' Lorem Ipsum ')->trimStart(); // 'Lorem Ipsum ' + u(' Lorem Ipsum ')->trimEnd(); // ' Lorem Ipsum' + + // removes the given content from the start/end of the string + u('file-image-0001.png')->trimPrefix('file-'); // 'image-0001.png' + u('file-image-0001.png')->trimPrefix('image-'); // 'file-image-0001.png' + u('file-image-0001.png')->trimPrefix('file-image-'); // '0001.png' + u('template.html.twig')->trimSuffix('.html'); // 'template.html.twig' + u('template.html.twig')->trimSuffix('.twig'); // 'template.html' + u('template.html.twig')->trimSuffix('.html.twig'); // 'template' + // when passing an array of prefix/suffix, only the first one found is trimmed + u('file-image-0001.png')->trimPrefix(['file-', 'image-']); // 'image-0001.png' + u('template.html.twig')->trimSuffix(['.twig', '.html']); // 'template.html' + +Methods to Search and Replace +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + // checks if the string starts/ends with the given string + u('https://symfony.com')->startsWith('https'); // true + u('report-1234.pdf')->endsWith('.pdf'); // true + + // checks if the string contents are exactly the same as the given contents + u('foo')->equalsTo('foo'); // true + + // checks if the string content match the given regular expression. + u('avatar-73647.png')->match('/avatar-(\d+)\.png/'); + // result = ['avatar-73647.png', '73647', null] + + // You can pass flags for preg_match() as second argument. If PREG_PATTERN_ORDER + // or PREG_SET_ORDER are passed, preg_match_all() will be used. + u('206-555-0100 and 800-555-1212')->match('/\d{3}-\d{3}-\d{4}/', \PREG_PATTERN_ORDER); + // result = [['206-555-0100', '800-555-1212']] + + // checks if the string contains any of the other given strings + u('aeiou')->containsAny('a'); // true + u('aeiou')->containsAny(['ab', 'efg']); // false + u('aeiou')->containsAny(['eio', 'foo', 'z']); // true + + // finds the position of the first occurrence of the given string + // (the second argument is the position where the search starts and negative + // values have the same meaning as in PHP functions) + u('abcdeabcde')->indexOf('c'); // 2 + u('abcdeabcde')->indexOf('c', 2); // 2 + u('abcdeabcde')->indexOf('c', -4); // 7 + u('abcdeabcde')->indexOf('eab'); // 4 + u('abcdeabcde')->indexOf('k'); // null + + // finds the position of the last occurrence of the given string + // (the second argument is the position where the search starts and negative + // values have the same meaning as in PHP functions) + u('abcdeabcde')->indexOfLast('c'); // 7 + u('abcdeabcde')->indexOfLast('c', 2); // 7 + u('abcdeabcde')->indexOfLast('c', -4); // 2 + u('abcdeabcde')->indexOfLast('eab'); // 4 + u('abcdeabcde')->indexOfLast('k'); // null + + // replaces all occurrences of the given string + u('http://symfony.com')->replace('http://', 'https://'); // 'https://symfony.com' + // replaces all occurrences of the given regular expression + u('(+1) 206-555-0100')->replaceMatches('/[^A-Za-z0-9]++/', ''); // '12065550100' + // you can pass a callable as the second argument to perform advanced replacements + u('123')->replaceMatches('/\d/', function ($match) { + return '['.$match[0].']'; + }); // result = '[1][2][3]' + +Methods to Join, Split, Truncate and Reverse +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + // uses the string as the "glue" to merge all the given strings + u(', ')->join(['foo', 'bar']); // 'foo, bar' + + // breaks the string into pieces using the given delimiter + u('template_name.html.twig')->split('.'); // ['template_name', 'html', 'twig'] + // you can set the maximum number of pieces as the second argument + u('template_name.html.twig')->split('.', 2); // ['template_name', 'html.twig'] + + // returns a substring which starts at the first argument and has the length of the + // second optional argument (negative values have the same meaning as in PHP functions) + u('Symfony is great')->slice(0, 7); // 'Symfony' + u('Symfony is great')->slice(0, -6); // 'Symfony is' + u('Symfony is great')->slice(11); // 'great' + u('Symfony is great')->slice(-5); // 'great' + + // reduces the string to the length given as argument (if it's longer) + u('Lorem Ipsum')->truncate(3); // 'Lor' + u('Lorem Ipsum')->truncate(80); // 'Lorem Ipsum' + // the second argument is the character(s) added when a string is cut + // (the total length includes the length of this character(s)) + u('Lorem Ipsum')->truncate(8, '…'); // 'Lorem I…' + // if the third argument is false, the last word before the cut is kept + // even if that generates a string longer than the desired length + u('Lorem Ipsum')->truncate(8, '…', false); // 'Lorem Ipsum' + +:: + + // breaks the string into lines of the given length + u('Lorem Ipsum')->wordwrap(4); // 'Lorem\nIpsum' + // by default it breaks by white space; pass TRUE to break unconditionally + u('Lorem Ipsum')->wordwrap(4, "\n", true); // 'Lore\nm\nIpsu\nm' + + // replaces a portion of the string with the given contents: + // the second argument is the position where the replacement starts; + // the third argument is the number of graphemes/code points removed from the string + u('0123456789')->splice('xxx'); // 'xxx' + u('0123456789')->splice('xxx', 0, 2); // 'xxx23456789' + u('0123456789')->splice('xxx', 0, 6); // 'xxx6789' + u('0123456789')->splice('xxx', 6); // '012345xxx' + + // breaks the string into pieces of the length given as argument + u('0123456789')->chunk(3); // ['012', '345', '678', '9'] + + // reverses the order of the string contents + u('foo bar')->reverse(); // 'rab oof' + u('さよなら')->reverse(); // 'らなよさ' + +Methods Added by ByteString +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These methods are only available for ``ByteString`` objects:: + + // returns TRUE if the string contents are valid UTF-8 contents + b('Lorem Ipsum')->isUtf8(); // true + b("\xc3\x28")->isUtf8(); // false + +Methods Added by CodePointString and UnicodeString +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These methods are only available for ``CodePointString`` and ``UnicodeString`` +objects:: + + // transliterates any string into the latin alphabet defined by the ASCII encoding + // (don't use this method to build a slugger because this component already provides + // a slugger, as explained later in this article) + u('नमस्ते')->ascii(); // 'namaste' + u('さよなら')->ascii(); // 'sayonara' + u('спасибо')->ascii(); // 'spasibo' + + // returns an array with the code point or points stored at the given position + // (code points of 'नमस्ते' graphemes = [2344, 2350, 2360, 2340] + u('नमस्ते')->codePointsAt(0); // [2344] + u('नमस्ते')->codePointsAt(2); // [2360] + +`Unicode equivalence`_ is the specification by the Unicode standard that +different sequences of code points represent the same character. For example, +the Swedish letter ``å`` can be a single code point (``U+00E5`` = *"latin small +letter A with ring above"*) or a sequence of two code points (``U+0061`` = +*"latin small letter A"* + ``U+030A`` = *"combining ring above"*). The +``normalize()`` method allows to pick the normalization mode:: + + // these encode the letter as a single code point: U+00E5 + u('å')->normalize(UnicodeString::NFC); + u('å')->normalize(UnicodeString::NFKC); + // these encode the letter as two code points: U+0061 + U+030A + u('å')->normalize(UnicodeString::NFD); + u('å')->normalize(UnicodeString::NFKD); + +Slugger +------- + +In some contexts, such as URLs and file/directory names, it's not safe to use +any Unicode character. A *slugger* transforms a given string into another string +that only includes safe ASCII characters:: + + use Symfony\Component\String\Slugger\AsciiSlugger; + + $slugger = new AsciiSlugger(); + $slug = $slugger->slug('Wôrķšƥáçè ~~sèťtïñğš~~'); + // $slug = 'Workspace-settings' + + // you can also pass an array with additional character substitutions + $slugger = new AsciiSlugger('en', ['en' => ['%' => 'percent', '€' => 'euro']]); + $slug = $slugger->slug('10% or 5€'); + // $slug = '10-percent-or-5-euro' + + // if there is no symbols map for your locale (e.g. 'en_GB') then the parent locale's symbols map + // will be used instead (i.e. 'en') + $slugger = new AsciiSlugger('en_GB', ['en' => ['%' => 'percent', '€' => 'euro']]); + $slug = $slugger->slug('10% or 5€'); + // $slug = '10-percent-or-5-euro' + + // for more dynamic substitutions, pass a PHP closure instead of an array + $slugger = new AsciiSlugger('en', function ($string, $locale) { + return str_replace('❤️', 'love', $string); + }); + +The separator between words is a dash (``-``) by default, but you can define +another separator as the second argument:: + + $slug = $slugger->slug('Wôrķšƥáçè ~~sèťtïñğš~~', '/'); + // $slug = 'Workspace/settings' + +The slugger transliterates the original string into the Latin script before +applying the other transformations. The locale of the original string is +detected automatically, but you can define it explicitly:: + + // this tells the slugger to transliterate from Korean language + $slugger = new AsciiSlugger('ko'); + + // you can override the locale as the third optional parameter of slug() + $slug = $slugger->slug('...', '-', 'fa'); + +In a Symfony application, you don't need to create the slugger yourself. Thanks +to :doc:`service autowiring `, you can inject a +slugger by type-hinting a service constructor argument with the +:class:`Symfony\\Component\\String\\Slugger\\SluggerInterface`. The locale of +the injected slugger is the same as the request locale:: + + use Symfony\Component\String\Slugger\SluggerInterface; + + class MyService + { + private $slugger; + + public function __construct(SluggerInterface $slugger) + { + $this->slugger = $slugger; + } + + public function someMethod() + { + $slug = $this->slugger->slug('...'); + } + } + +.. _string-slugger-emoji: + +Slug Emojis +~~~~~~~~~~~ + +.. versionadded:: 6.2 + + The Emoji transliteration feature was introduced in Symfony 6.2. + +You can transform any emojis into their textual representation:: + + use Symfony\Component\String\Slugger\AsciiSlugger; + + $slugger = new AsciiSlugger(); + $slugger = $slugger->withEmoji(); + + $slug = $slugger->slug('a 😺, 🐈‍⬛, and a 🦁 go to 🏞️', '-', 'en'); + // $slug = 'a-grinning-cat-black-cat-and-a-lion-go-to-national-park'; + + $slug = $slugger->slug('un 😺, 🐈‍⬛, et un 🦁 vont au 🏞️', '-', 'fr'); + // $slug = 'un-chat-qui-sourit-chat-noir-et-un-tete-de-lion-vont-au-parc-national'; + +If you want to use a specific locale for the emoji, or to use the short codes +from GitHub or Slack, use the first argument of ``withEmoji()`` method:: + + use Symfony\Component\String\Slugger\AsciiSlugger; + + $slugger = new AsciiSlugger(); + $slugger = $slugger->withEmoji('github'); // or "en", or "fr", etc. + + $slug = $slugger->slug('a 😺, 🐈‍⬛, and a 🦁'); + // $slug = 'a-smiley-cat-black-cat-and-a-lion'; + +.. _string-inflector: + +Inflector +--------- + +In some scenarios such as code generation and code introspection, you need to +convert words from/to singular/plural. For example, to know the property +associated with an *adder* method, you must convert from plural +(``addStories()`` method) to singular (``$story`` property). + +Most human languages have simple pluralization rules, but at the same time they +define lots of exceptions. For example, the general rule in English is to add an +``s`` at the end of the word (``book`` -> ``books``) but there are lots of +exceptions even for common words (``woman`` -> ``women``, ``life`` -> ``lives``, +``news`` -> ``news``, ``radius`` -> ``radii``, etc.) + +This component provides an :class:`Symfony\\Component\\String\\Inflector\\EnglishInflector` +class to convert English words from/to singular/plural with confidence:: + + use Symfony\Component\String\Inflector\EnglishInflector; + + $inflector = new EnglishInflector(); + + $result = $inflector->singularize('teeth'); // ['tooth'] + $result = $inflector->singularize('radii'); // ['radius'] + $result = $inflector->singularize('leaves'); // ['leaf', 'leave', 'leaff'] + + $result = $inflector->pluralize('bacterium'); // ['bacteria'] + $result = $inflector->pluralize('news'); // ['news'] + $result = $inflector->pluralize('person'); // ['persons', 'people'] + +The value returned by both methods is always an array because sometimes it's not +possible to determine a unique singular/plural form for the given word. + +.. _`ASCII`: https://en.wikipedia.org/wiki/ASCII +.. _`Unicode`: https://en.wikipedia.org/wiki/Unicode +.. _`Code points`: https://en.wikipedia.org/wiki/Code_point +.. _`Grapheme clusters`: https://en.wikipedia.org/wiki/Grapheme +.. _`Unicode equivalence`: https://en.wikipedia.org/wiki/Unicode_equivalence diff --git a/components/uid.rst b/components/uid.rst new file mode 100644 index 00000000000..ef226ac89a2 --- /dev/null +++ b/components/uid.rst @@ -0,0 +1,500 @@ +.. index:: + single: UID + single: Components; UID + +The UID Component +================= + + The UID component provides utilities to work with `unique identifiers`_ (UIDs) + such as UUIDs and ULIDs. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/uid + +.. include:: /components/require_autoload.rst.inc + +.. _uuid: + +UUIDs +----- + +`UUIDs`_ (*universally unique identifiers*) are one of the most popular UIDs in +the software industry. UUIDs are 128-bit numbers usually represented as five +groups of hexadecimal characters: ``xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx`` +(the ``M`` digit is the UUID version and the ``N`` digit is the UUID variant). + +Generating UUIDs +~~~~~~~~~~~~~~~~ + +Use the named constructors of the ``Uuid`` class or any of the specific classes +to create each type of UUID:: + + use Symfony\Component\Uid\Uuid; + + // UUID type 1 generates the UUID using the MAC address of your device and a timestamp. + // Both are obtained automatically, so you don't have to pass any constructor argument. + $uuid = Uuid::v1(); // $uuid is an instance of Symfony\Component\Uid\UuidV1 + + // UUID type 4 generates a random UUID, so you don't have to pass any constructor argument. + $uuid = Uuid::v4(); // $uuid is an instance of Symfony\Component\Uid\UuidV4 + + // UUID type 3 and 5 generate a UUID hashing the given namespace and name. Type 3 uses + // MD5 hashes and Type 5 uses SHA-1. The namespace is another UUID (e.g. a Type 4 UUID) + // and the name is an arbitrary string (e.g. a product name; if it's unique). + $namespace = Uuid::v4(); + $name = $product->getUniqueName(); + + $uuid = Uuid::v3($namespace, $name); // $uuid is an instance of Symfony\Component\Uid\UuidV3 + $uuid = Uuid::v5($namespace, $name); // $uuid is an instance of Symfony\Component\Uid\UuidV5 + + // the namespaces defined by RFC 4122 (see https://tools.ietf.org/html/rfc4122#appendix-C) + // are available as PHP constants and as string values + $uuid = Uuid::v3(Uuid::NAMESPACE_DNS, $name); // same as: Uuid::v3('dns', $name); + $uuid = Uuid::v3(Uuid::NAMESPACE_URL, $name); // same as: Uuid::v3('url', $name); + $uuid = Uuid::v3(Uuid::NAMESPACE_OID, $name); // same as: Uuid::v3('oid', $name); + $uuid = Uuid::v3(Uuid::NAMESPACE_X500, $name); // same as: Uuid::v3('x500', $name); + + // UUID type 6 is not part of the UUID standard. It's lexicographically sortable + // (like ULIDs) and contains a 60-bit timestamp and 63 extra unique bits. + // It's defined in http://gh.peabody.io/uuidv6/ + $uuid = Uuid::v6(); // $uuid is an instance of Symfony\Component\Uid\UuidV6 + + // UUID version 7 features a time-ordered value field derived from the well known + // Unix Epoch timestamp source: the number of seconds since midnight 1 Jan 1970 UTC, leap seconds excluded. + // As well as improved entropy characteristics over versions 1 or 6. + $uuid = Uuid::v7(); + + // UUID version 8 provides an RFC-compatible format for experimental or vendor-specific use cases. + // The only requirement is that the variant and version bits MUST be set as defined in Section 4: + // https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#variant_and_version_fields + // UUIDv8 uniqueness will be implementation-specific and SHOULD NOT be assumed. + $uuid = Uuid::v8(); + +.. versionadded:: 6.2 + + UUID versions 7 and 8 were introduced in Symfony 6.2. + +If your UUID value is already generated in another format, use any of the +following methods to create a ``Uuid`` object from it:: + + // all the following examples would generate the same Uuid object + $uuid = Uuid::fromString('d9e7a184-5d5b-11ea-a62a-3499710062d0'); + $uuid = Uuid::fromBinary("\xd9\xe7\xa1\x84\x5d\x5b\x11\xea\xa6\x2a\x34\x99\x71\x00\x62\xd0"); + $uuid = Uuid::fromBase32('6SWYGR8QAV27NACAHMK5RG0RPG'); + $uuid = Uuid::fromBase58('TuetYWNHhmuSQ3xPoVLv9M'); + $uuid = Uuid::fromRfc4122('d9e7a184-5d5b-11ea-a62a-3499710062d0'); + +Converting UUIDs +~~~~~~~~~~~~~~~~ + +Use these methods to transform the UUID object into different bases:: + + $uuid = Uuid::fromString('d9e7a184-5d5b-11ea-a62a-3499710062d0'); + + $uuid->toBinary(); // string(16) "\xd9\xe7\xa1\x84\x5d\x5b\x11\xea\xa6\x2a\x34\x99\x71\x00\x62\xd0" + $uuid->toBase32(); // string(26) "6SWYGR8QAV27NACAHMK5RG0RPG" + $uuid->toBase58(); // string(22) "TuetYWNHhmuSQ3xPoVLv9M" + $uuid->toRfc4122(); // string(36) "d9e7a184-5d5b-11ea-a62a-3499710062d0" + $uuid->toHex(); // string(34) "0xd9e7a1845d5b11eaa62a3499710062d0" + +.. versionadded:: 6.2 + + The ``toHex()`` method was introduced in Symfony 6.2. + +Working with UUIDs +~~~~~~~~~~~~~~~~~~ + +UUID objects created with the ``Uuid`` class can use the following methods +(which are equivalent to the ``uuid_*()`` method of the PHP extension):: + + use Symfony\Component\Uid\NilUuid; + use Symfony\Component\Uid\Uuid; + + // checking if the UUID is null (note that the class is called + // NilUuid instead of NullUuid to follow the UUID standard notation) + $uuid = Uuid::v4(); + $uuid instanceof NilUuid; // false + + // checking the type of UUID + use Symfony\Component\Uid\UuidV4; + $uuid = Uuid::v4(); + $uuid instanceof UuidV4; // true + + // getting the UUID datetime (it's only available in certain UUID types) + $uuid = Uuid::v1(); + $uuid->getDateTime(); // returns a \DateTimeImmutable instance + + // checking if a given value is valid as UUID + $isValid = Uuid::isValid($uuid); // true or false + + // comparing UUIDs and checking for equality + $uuid1 = Uuid::v1(); + $uuid4 = Uuid::v4(); + $uuid1->equals($uuid4); // false + + // this method returns: + // * int(0) if $uuid1 and $uuid4 are equal + // * int > 0 if $uuid1 is greater than $uuid4 + // * int < 0 if $uuid1 is less than $uuid4 + $uuid1->compare($uuid4); // e.g. int(4) + +Storing UUIDs in Databases +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you :doc:`use Doctrine `, consider using the ``uuid`` Doctrine +type, which converts to/from UUID objects automatically:: + + // src/Entity/Product.php + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Bridge\Doctrine\Types\UuidType; + + #[ORM\Entity(repositoryClass: ProductRepository::class)] + class Product + { + #[ORM\Column(type: UuidType::NAME)] + private $someProperty; + + // ... + } + +.. versionadded:: 6.2 + + The ``UuidType::NAME`` constant was introduced in Symfony 6.2. + +There's also a Doctrine generator to help auto-generate UUID values for the +entity primary keys:: + + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Bridge\Doctrine\Types\UuidType; + use Symfony\Component\Uid\Uuid; + + class User implements UserInterface + { + #[ORM\Id] + #[ORM\Column(type: UuidType::NAME, unique: true)] + #[ORM\GeneratedValue(strategy: 'CUSTOM')] + #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')] + private $id; + + public function getId(): ?Uuid + { + return $this->id; + } + + // ... + } + +When using built-in Doctrine repository methods (e.g. ``findOneBy()``), Doctrine +knows how to convert these UUID types to build the SQL query +(e.g. ``->findOneBy(['user' => $user->getUuid()])``). However, when using DQL +queries or building the query yourself, you'll need to set ``uuid`` as the type +of the UUID parameters:: + + // src/Repository/ProductRepository.php + + // ... + use Symfony\Bridge\Doctrine\Types\UuidType; + + class ProductRepository extends ServiceEntityRepository + { + // ... + + public function findUserProducts(User $user): array + { + $qb = $this->createQueryBuilder('p') + // ... + // add UuidType::NAME as the third argument to tell Doctrine that this is a UUID + ->setParameter('user', $user->getUuid(), UuidType::NAME) + + // alternatively, you can convert it to a value compatible with + // the type inferred by Doctrine + ->setParameter('user', $user->getUuid()->toBinary()) + ; + + // ... + } + } + +.. _ulid: + +ULIDs +----- + +`ULIDs`_ (*Universally Unique Lexicographically Sortable Identifier*) are 128-bit +numbers usually represented as a 26-character string: ``TTTTTTTTTTRRRRRRRRRRRRRRRR`` +(where ``T`` represents a timestamp and ``R`` represents the random bits). + +ULIDs are an alternative to UUIDs when using those is impractical. They provide +128-bit compatibility with UUID, they are lexicographically sortable and they +are encoded as 26-character strings (vs 36-character UUIDs). + +.. note:: + + If you generate more than one ULID during the same millisecond in the + same process then the random portion is incremented by one bit in order + to provide monotonicity for sorting. The random portion is not random + compared to the previous ULID in this case. + +Generating ULIDs +~~~~~~~~~~~~~~~~ + +Instantiate the ``Ulid`` class to generate a random ULID value:: + + use Symfony\Component\Uid\Ulid; + + $ulid = new Ulid(); // e.g. 01AN4Z07BY79KA1307SR9X4MV3 + +If your ULID value is already generated in another format, use any of the +following methods to create a ``Ulid`` object from it:: + + // all the following examples would generate the same Ulid object + $ulid = Ulid::fromString('01E439TP9XJZ9RPFH3T1PYBCR8'); + $ulid = Ulid::fromBinary("\x01\x71\x06\x9d\x59\x3d\x97\xd3\x8b\x3e\x23\xd0\x6d\xe5\xb3\x08"); + $ulid = Ulid::fromBase32('01E439TP9XJZ9RPFH3T1PYBCR8'); + $ulid = Ulid::fromBase58('1BKocMc5BnrVcuq2ti4Eqm'); + $ulid = Ulid::fromRfc4122('0171069d-593d-97d3-8b3e-23d06de5b308'); + +There's also a special ``NilUlid`` class to represent ULID ``null`` values:: + + use Symfony\Component\Uid\NilUlid; + + $ulid = new NilUlid(); + // equivalent to $ulid = new Ulid('00000000000000000000000000'); + +Converting ULIDs +~~~~~~~~~~~~~~~~ + +Use these methods to transform the ULID object into different bases:: + + $ulid = Ulid::fromString('01E439TP9XJZ9RPFH3T1PYBCR8'); + + $ulid->toBinary(); // string(16) "\x01\x71\x06\x9d\x59\x3d\x97\xd3\x8b\x3e\x23\xd0\x6d\xe5\xb3\x08" + $ulid->toBase32(); // string(26) "01E439TP9XJZ9RPFH3T1PYBCR8" + $ulid->toBase58(); // string(22) "1BKocMc5BnrVcuq2ti4Eqm" + $ulid->toRfc4122(); // string(36) "0171069d-593d-97d3-8b3e-23d06de5b308" + $ulid->toHex(); // string(34) "0x0171069d593d97d38b3e23d06de5b308" + +.. versionadded:: 6.2 + + The ``toHex()`` method was introduced in Symfony 6.2. + +Working with ULIDs +~~~~~~~~~~~~~~~~~~ + +ULID objects created with the ``Ulid`` class can use the following methods:: + + use Symfony\Component\Uid\Ulid; + + $ulid1 = new Ulid(); + $ulid2 = new Ulid(); + + // checking if a given value is valid as ULID + $isValid = Ulid::isValid($ulidValue); // true or false + + // getting the ULID datetime + $ulid1->getDateTime(); // returns a \DateTimeImmutable instance + + // comparing ULIDs and checking for equality + $ulid1->equals($ulid2); // false + // this method returns $ulid1 <=> $ulid2 + $ulid1->compare($ulid2); // e.g. int(-1) + +Storing ULIDs in Databases +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you :doc:`use Doctrine `, consider using the ``ulid`` Doctrine +type, which converts to/from ULID objects automatically:: + + // src/Entity/Product.php + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Bridge\Doctrine\Types\UlidType; + + #[ORM\Entity(repositoryClass: ProductRepository::class)] + class Product + { + #[ORM\Column(type: UlidType::NAME)] + private $someProperty; + + // ... + } + +.. versionadded:: 6.2 + + The ``UlidType::NAME`` constant was introduced in Symfony 6.2. + +There's also a Doctrine generator to help auto-generate ULID values for the +entity primary keys:: + + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Bridge\Doctrine\Types\UlidType; + use Symfony\Component\Uid\Ulid; + + class Product + { + #[ORM\Id] + #[ORM\Column(type: UlidType::NAME, unique: true)] + #[ORM\GeneratedValue(strategy: 'CUSTOM')] + #[ORM\CustomIdGenerator(class: 'doctrine.ulid_generator')] + private $id; + + public function getId(): ?Ulid + { + return $this->id; + } + + // ... + + } + +When using built-in Doctrine repository methods (e.g. ``findOneBy()``), Doctrine +knows how to convert these ULID types to build the SQL query +(e.g. ``->findOneBy(['user' => $user->getUlid()])``). However, when using DQL +queries or building the query yourself, you'll need to set ``ulid`` as the type +of the ULID parameters:: + + // src/Repository/ProductRepository.php + + // ... + use Symfony\Bridge\Doctrine\Types\UlidType; + + class ProductRepository extends ServiceEntityRepository + { + // ... + + public function findUserProducts(User $user): array + { + $qb = $this->createQueryBuilder('p') + // ... + // add UlidType::NAME as the third argument to tell Doctrine that this is a ULID + ->setParameter('user', $user->getUlid(), UlidType::NAME) + + // alternatively, you can convert it to a value compatible with + // the type inferred by Doctrine + ->setParameter('user', $user->getUlid()->toBinary()) + ; + + // ... + } + } + +Generating and Inspecting UUIDs/ULIDs in the Console +---------------------------------------------------- + +This component provides several commands to generate and inspect UUIDs/ULIDs in +the console. They are not enabled by default, so you must add the following +configuration in your application before using these commands: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + Symfony\Component\Uid\Command\GenerateUlidCommand: ~ + Symfony\Component\Uid\Command\GenerateUuidCommand: ~ + Symfony\Component\Uid\Command\InspectUlidCommand: ~ + Symfony\Component\Uid\Command\InspectUuidCommand: ~ + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\Uid\Command\GenerateUlidCommand; + use Symfony\Component\Uid\Command\GenerateUuidCommand; + use Symfony\Component\Uid\Command\InspectUlidCommand; + use Symfony\Component\Uid\Command\InspectUuidCommand; + + return static function (ContainerConfigurator $configurator): void { + // ... + + $services + ->set(GenerateUlidCommand::class) + ->set(GenerateUuidCommand::class) + ->set(InspectUlidCommand::class) + ->set(InspectUuidCommand::class); + }; + +Now you can generate UUIDs/ULIDs as follows (add the ``--help`` option to the +commands to learn about all their options): + +.. code-block:: terminal + + # generate 1 random-based UUID + $ php bin/console uuid:generate --random-based + + # generate 1 time-based UUID with a specific node + $ php bin/console uuid:generate --time-based=now --node=fb3502dc-137e-4849-8886-ac90d07f64a7 + + # generate 2 UUIDs and output them in base58 format + $ php bin/console uuid:generate --count=2 --format=base58 + + # generate 1 ULID with the current time as the timestamp + $ php bin/console ulid:generate + + # generate 1 ULID with a specific timestamp + $ php bin/console ulid:generate --time="2021-02-02 14:00:00" + + # generate 2 ULIDs and ouput them in RFC4122 format + $ php bin/console ulid:generate --count=2 --format=rfc4122 + +In addition to generating new UIDs, you can also inspect them with the following +commands to show all the information for a given UID: + +.. code-block:: terminal + + $ php bin/console uuid:inspect d0a3a023-f515-4fe0-915c-575e63693998 + ---------------------- -------------------------------------- + Label Value + ---------------------- -------------------------------------- + Version 4 + Canonical (RFC 4122) d0a3a023-f515-4fe0-915c-575e63693998 + Base 58 SmHvuofV4GCF7QW543rDD9 + Base 32 6GMEG27X8N9ZG92Q2QBSHPJECR + ---------------------- -------------------------------------- + + $ php bin/console ulid:inspect 01F2TTCSYK1PDRH73Z41BN1C4X + --------------------- -------------------------------------- + Label Value + --------------------- -------------------------------------- + Canonical (Base 32) 01F2TTCSYK1PDRH73Z41BN1C4X + Base 58 1BYGm16jS4kX3VYCysKKq6 + RFC 4122 0178b5a6-67d3-0d9b-889c-7f205750b09d + --------------------- -------------------------------------- + Timestamp 2021-04-09 08:01:24.947 + --------------------- -------------------------------------- + +.. _`unique identifiers`: https://en.wikipedia.org/wiki/UID +.. _`UUIDs`: https://en.wikipedia.org/wiki/Universally_unique_identifier +.. _`ULIDs`: https://github.com/ulid/spec diff --git a/components/validator/resources.rst b/components/validator/resources.rst index 4de84a7cb49..e32ed08d9e0 100644 --- a/components/validator/resources.rst +++ b/components/validator/resources.rst @@ -106,14 +106,15 @@ prefixed classes included in doc block comments (``/** ... */``). For example:: } To enable the annotation loader, call the -:method:`Symfony\\Component\\Validator\\ValidatorBuilder::enableAnnotationMapping` -method. It takes an optional annotation reader instance, which defaults to -``Doctrine\Common\Annotations\AnnotationReader``:: +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::enableAnnotationMapping` method. +If you use annotations instead of attributes, it's also required to call +``addDefaultDoctrineAnnotationReader()`` to use Doctrine's annotation reader:: use Symfony\Component\Validator\Validation; $validator = Validation::createValidatorBuilder() ->enableAnnotationMapping() + ->addDefaultDoctrineAnnotationReader() // add this only when using annotations ->getValidator(); To disable the annotation loader after it was enabled, call @@ -134,7 +135,8 @@ multiple mappings:: use Symfony\Component\Validator\Validation; $validator = Validation::createValidatorBuilder() - ->enableAnnotationMapping() + ->enableAnnotationMapping(true) + ->addDefaultDoctrineAnnotationReader() ->addMethodMapping('loadValidatorMetadata') ->addXmlMapping('validator/validation.xml') ->getValidator(); @@ -158,10 +160,6 @@ implement the PSR-6 interface :class:`Psr\\Cache\\CacheItemPoolInterface`):: ->setMappingCache(new SomePsr6Cache()) ->getValidator(); -.. versionadded:: 4.4 - - Support for PSR-6 compatible mapping caches was introduced in Symfony 4.4. - .. note:: The loaders already use a singleton load mechanism. That means that the diff --git a/components/var_dumper.rst b/components/var_dumper.rst index 1202791b97c..3453853b411 100644 --- a/components/var_dumper.rst +++ b/components/var_dumper.rst @@ -66,7 +66,7 @@ current PHP SAPI: You can also select the output format explicitly defining the ``VAR_DUMPER_FORMAT`` environment variable and setting its value to either - ``html`` or ``cli``. + ``html``, ``cli`` or :ref:`server `. .. note:: @@ -191,6 +191,35 @@ Then you can use the following command to start a server out-of-the-box: $ ./vendor/bin/var-dump-server [OK] Server listening on tcp://127.0.0.1:9912 +.. _var-dumper-dump-server-format: + +Configuring the Dump Server with Environment Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you prefer to not modify the application configuration (e.g. to quickly debug +a project given to you) use the ``VAR_DUMPER_FORMAT`` env var. + +First, start the server as usual: + +.. code-block:: terminal + + $ ./vendor/bin/var-dump-server + +Then, run your code with the ``VAR_DUMPER_FORMAT=server`` env var by configuring +this value in the :ref:`.env file of your application `. For +console commands, you can also define this env var as follows: + +.. code-block:: terminal + + $ VAR_DUMPER_FORMAT=server [your-cli-command] + +.. note:: + + The host used by the ``server`` format is the one configured in the + ``VAR_DUMPER_SERVER`` env var or ``127.0.0.1:9912`` if none is defined. + If you prefer, you can also configure the host in the ``VAR_DUMPER_FORMAT`` + env var like this: ``VAR_DUMPER_FORMAT=tcp://127.0.0.1:1234``. + DebugBundle and Twig Integration -------------------------------- @@ -230,11 +259,7 @@ option. Read more about this and other options in finished, press ``Esc.`` to hide the box again. If you want to use your browser search input, press ``Ctrl. + F`` or - ``Cmd. + F`` again while having focus on VarDumper's search input. - - .. versionadded:: 4.4 - - The feature to use the browser search input was introduced in Symfony 4.4. + ``Cmd. + F`` again while focusing on VarDumper's search input. Using the VarDumper Component in your PHPUnit Test Suite -------------------------------------------------------- @@ -263,11 +288,6 @@ The ``VarDumperTestTrait`` also includes these other methods: is called automatically after each case to reset the custom configuration made in ``setUpVarDumper()``. -.. versionadded:: 4.4 - - The ``setUpVarDumper()`` and ``tearDownVarDumper()`` methods were introduced - in Symfony 4.4. - Example:: use PHPUnit\Framework\TestCase; diff --git a/components/yaml.rst b/components/yaml.rst index ba6c0849db2..2c463d1c731 100644 --- a/components/yaml.rst +++ b/components/yaml.rst @@ -338,6 +338,35 @@ syntax to parse them as proper PHP constants:: $parameters = Yaml::parse($yaml, Yaml::PARSE_CONSTANT); // $parameters = ['foo' => 'PHP_INT_SIZE', 'bar' => 8]; +Parsing PHP Enumerations +~~~~~~~~~~~~~~~~~~~~~~~~ + +The YAML parser supports `PHP enumerations`_, both unit and backed enums. +By default, they are parsed as regular strings. Use the ``PARSE_CONSTANT`` flag +and the special ``!php/enum`` syntax to parse them as proper PHP enums:: + + enum FooEnum: string + { + case Foo = 'foo'; + case Bar = 'bar'; + } + + // ... + + $yaml = '{ foo: FooEnum::Foo, bar: !php/enum FooEnum::Foo }'; + $parameters = Yaml::parse($yaml, Yaml::PARSE_CONSTANT); + // the value of the 'foo' key is a string because it missed the `!php/enum` syntax + // $parameters = ['foo' => 'FooEnum::Foo', 'bar' => FooEnum::Foo]; + + $yaml = '{ foo: FooEnum::Foo, bar: !php/enum FooEnum::Foo->value }'; + $parameters = Yaml::parse($yaml, Yaml::PARSE_CONSTANT); + // the value of the 'foo' key is a string because it missed the `!php/enum` syntax + // $parameters = ['foo' => 'FooEnum::Foo', 'bar' => 'foo']; + +.. versionadded:: 6.2 + + The support for PHP enumerations was introduced in Symfony 6.2. + Parsing and Dumping of Binary Data ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -389,10 +418,6 @@ you can dump them as ``~`` with the ``DUMP_NULL_AS_TILDE`` flag:: $dumped = Yaml::dump(['foo' => null], 2, 4, Yaml::DUMP_NULL_AS_TILDE); // foo: ~ -.. versionadded:: 4.4 - - The flag to dump ``null`` as ``~`` was introduced in Symfony 4.4. - Syntax Validation ~~~~~~~~~~~~~~~~~ @@ -436,6 +461,9 @@ Then, execute the script for validating contents: # or contents passed to STDIN $ cat path/to/file.yaml | php lint.php + # you can also exclude one or more files from linting + $ php lint.php path/to/directory --exclude=path/to/directory/foo.yaml --exclude=path/to/directory/bar.yaml + The result is written to STDOUT and uses a plain text format by default. Add the ``--format`` option to get the output in JSON format: @@ -461,3 +489,4 @@ Learn More .. _`YAML`: https://yaml.org/ .. _`YAML 1.2 version specification`: https://yaml.org/spec/1.2/spec.html .. _`ISO-8601`: https://www.iso.org/iso-8601-date-and-time-format.html +.. _`PHP enumerations`: https://www.php.net/manual/en/language.types.enumerations.php diff --git a/components/yaml/yaml_format.rst b/components/yaml/yaml_format.rst index d2b7e62e5d2..aa4ef007beb 100644 --- a/components/yaml/yaml_format.rst +++ b/components/yaml/yaml_format.rst @@ -122,7 +122,7 @@ Numbers .. code-block:: yaml # an octal - 014 + 0o14 .. code-block:: yaml diff --git a/configuration.rst b/configuration.rst index e7f2680e917..608ac39e19c 100644 --- a/configuration.rst +++ b/configuration.rst @@ -71,8 +71,18 @@ readable. These are the main advantages and disadvantages of each format: and validation for it. :doc:`Learn the YAML syntax `; * **XML**: autocompleted/validated by most IDEs and is parsed natively by PHP, but sometimes it generates configuration considered too verbose. `Learn the XML syntax`_; -* **PHP**: very powerful and it allows you to create dynamic configuration, but the - resulting configuration is less readable than the other formats. +* **PHP**: very powerful and it allows you to create dynamic configuration with + arrays or a :ref:`ConfigBuilder `. + +.. note:: + + By default Symfony loads the configuration files defined in YAML and PHP + formats. If you define configuration in XML format, update the + ``src/Kernel.php`` file to add support for the ``.xml`` file extension. + + .. versionadded:: 6.1 + + The automatic loading of PHP configuration files was introduced in Symfony 6.1. Importing Configuration Files ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -144,10 +154,6 @@ configuration files, even if they use a different format: // ... -.. versionadded:: 4.4 - - The ``not_found`` option value for ``ignore_errors`` was introduced in Symfony 4.4. - .. _config-parameter-intro: .. _config-parameters-yml: .. _configuration-parameters: @@ -407,6 +413,90 @@ In reality, each environment differs only somewhat from others. This means that all environments share a large base of common configuration, which is put in files directly in the ``config/packages/`` directory. +.. tip:: + + You can also define options for different environments in a single + configuration file using the special ``when`` keyword: + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/webpack_encore.yaml + webpack_encore: + # ... + output_path: '%kernel.project_dir%/public/build' + strict_mode: true + cache: false + + # cache is enabled only in the "prod" environment + when@prod: + webpack_encore: + cache: true + + # disable strict mode only in the "test" environment + when@test: + webpack_encore: + strict_mode: false + + # YAML syntax allows to reuse contents using "anchors" (&some_name) and "aliases" (*some_name). + # In this example, 'test' configuration uses the exact same configuration as in 'prod' + when@prod: &webpack_prod + webpack_encore: + # ... + when@test: *webpack_prod + + .. code-block:: xml + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Config\WebpackEncoreConfig; + + return static function (WebpackEncoreConfig $webpackEncore, ContainerConfigurator $container) { + $webpackEncore + ->outputPath('%kernel.project_dir%/public/build') + ->strictMode(true) + ->cache(false) + ; + + // cache is enabled only in the "prod" environment + if ('prod' === $container->env()) { + $webpackEncore->cache(true); + } + + // disable strict mode only in the "test" environment + if ('test' === $container->env()) { + $webpackEncore->strictMode(false); + } + }; + .. seealso:: See the ``configureContainer()`` method of @@ -481,9 +571,10 @@ configure options that depend on where the application is run (e.g. the database credentials are usually different in production versus your local machine). If the values are sensitive, you can even :doc:`encrypt them as secrets `. -You can reference environment variables using the special syntax -``%env(ENV_VAR_NAME)%``. The values of these options are resolved at runtime -(only once per request, to not impact performance). +Use the special syntax ``%env(ENV_VAR_NAME)%`` to reference environment variables. +The values of these options are resolved at runtime (only once per request, to +not impact performance) so you can change the application behavior without having +to clear the cache. This example shows how you could configure the database connection using an env var: @@ -527,6 +618,8 @@ This example shows how you could configure the database connection using an env 'dbal' => [ // by convention the env var names are always uppercase 'url' => '%env(resolve:DATABASE_URL)%', + // or + 'url' => env('DATABASE_URL')->resolve(), ], ]); }; @@ -548,6 +641,14 @@ To define the value of an env var, you have several options: Some hosts - like SymfonyCloud - offer easy `utilities to manage env vars`_ in production. +.. note:: + + Some configuration features are not compatible with env vars. For example, + defining some container parameters conditionally based on the existence of + another configuration option. When using an env var, the configuration option + always exists, because its value will be ``null`` when the related env var + is not defined. + .. caution:: Beware that dumping the contents of the ``$_SERVER`` and ``$_ENV`` variables @@ -627,10 +728,6 @@ Define a default value in case the environment variable is not set: DB_USER= DB_PASS=${DB_USER:-root}pass # results in DB_PASS=rootpass -.. versionadded:: 4.4 - - The support for default values has been introduced in Symfony 4.4. - Embed commands via ``$()`` (not supported on Windows): .. code-block:: bash @@ -724,8 +821,44 @@ you can encrypt the value using the :doc:`secrets management system ` for all the +bundles installed in your application. By convention they all live in the +namespace ``Symfony\Config``:: + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security) { + $security->firewall('main') + ->pattern('^/*') + ->lazy(true) + ->anonymous(); + + $security + ->roleHierarchy('ROLE_ADMIN', ['ROLE_USER']) + ->roleHierarchy('ROLE_SUPER_ADMIN', ['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH']) + ->accessControl() + ->path('^/user') + ->role('ROLE_USER'); + + $security->accessControl(['path' => '^/admin', 'roles' => 'ROLE_ADMIN']); + }; + +.. note:: + + Only root classes in the namespace ``Symfony\Config`` are ConfigBuilders. + Nested configs (e.g. ``\Symfony\Config\Framework\CacheConfig``) are regular + PHP objects which aren't autowired when using them as an argument type. + Keep Going! ----------- diff --git a/configuration/env_var_processors.rst b/configuration/env_var_processors.rst index e8421290481..658a05163df 100644 --- a/configuration/env_var_processors.rst +++ b/configuration/env_var_processors.rst @@ -44,11 +44,17 @@ processor to turn the value of the ``HTTP_PORT`` env var into an integer: .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'router' => [ - 'http_port' => '%env(int:HTTP_PORT)%', - ], - ]); + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->router() + ->httpPort('%env(int:HTTP_PORT)%') + // or + ->httpPort(env('HTTP_PORT')->int()) + ; + }; Built-In Environment Variable Processors ---------------------------------------- @@ -90,10 +96,15 @@ Symfony provides the following env var processors: .. code-block:: php // config/packages/framework.php - $container->setParameter('env(SECRET)', 'some_secret'); - $container->loadFromExtension('framework', [ - 'secret' => '%env(string:SECRET)%', - ]); + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container, FrameworkConfig $framework) { + $container->setParameter('env(SECRET)', 'some_secret'); + $framework->secret(env('SECRET')->string()); + }; ``env(bool:FOO)`` Casts ``FOO`` to a bool (``true`` values are ``'true'``, ``'on'``, ``'yes'`` @@ -131,10 +142,50 @@ Symfony provides the following env var processors: .. code-block:: php // config/packages/framework.php - $container->setParameter('env(HTTP_METHOD_OVERRIDE)', 'true'); - $container->loadFromExtension('framework', [ - 'http_method_override' => '%env(bool:HTTP_METHOD_OVERRIDE)%', - ]); + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container, FrameworkConfig $framework) { + $container->setParameter('env(HTTP_METHOD_OVERRIDE)', 'true'); + $framework->httpMethodOverride(env('HTTP_METHOD_OVERRIDE')->bool()); + }; + +``env(not:FOO)`` + Casts ``FOO`` to a bool (just as ``env(bool:...)`` does) except it returns the inverted value + (falsy values are returned as ``true``, truthy values are returned as ``false``): + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + safe_for_production: '%env(not:APP_DEBUG)%' + + .. code-block:: xml + + + + + + + %env(not:APP_DEBUG)% + + + + + .. code-block:: php + + // config/services.php + $container->setParameter('safe_for_production', '%env(not:APP_DEBUG)%'); ``env(int:FOO)`` Casts ``FOO`` to an int. @@ -164,7 +215,9 @@ Symfony provides the following env var processors: xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:security="http://symfony.com/schema/dic/security" xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> Symfony\Component\HttpFoundation\Request::METHOD_HEAD @@ -178,15 +231,15 @@ Symfony provides the following env var processors: .. code-block:: php // config/packages/security.php - $container->setParameter('env(HEALTH_CHECK_METHOD)', 'Symfony\Component\HttpFoundation\Request::METHOD_HEAD'); - $container->loadFromExtension('security', [ - 'access_control' => [ - [ - 'path' => '^/health-check$', - 'methods' => '%env(const:HEALTH_CHECK_METHOD)%', - ], - ], - ]); + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\SecurityConfig; + + return static function (ContainerBuilder $container, SecurityConfig $security) { + $container->setParameter('env(HEALTH_CHECK_METHOD)', 'Symfony\Component\HttpFoundation\Request::METHOD_HEAD'); + $security->accessControl() + ->path('^/health-check$') + ->methods([env('HEALTH_CHECK_METHOD')->const()]); + }; ``env(base64:FOO)`` Decodes the content of ``FOO``, which is a base64 encoded string. @@ -227,10 +280,15 @@ Symfony provides the following env var processors: .. code-block:: php // config/packages/framework.php - $container->setParameter('env(TRUSTED_HOSTS)', '["10.0.0.1", "10.0.0.2"]'); - $container->loadFromExtension('framework', [ - 'trusted_hosts' => '%env(json:TRUSTED_HOSTS)%', - ]); + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container, FrameworkConfig $framework) { + $container->setParameter('env(TRUSTED_HOSTS)', '["10.0.0.1", "10.0.0.2"]'); + $framework->trustedHosts(env('TRUSTED_HOSTS')->json()); + }; ``env(resolve:FOO)`` If the content of ``FOO`` includes container parameters (with the syntax @@ -311,10 +369,68 @@ Symfony provides the following env var processors: .. code-block:: php // config/packages/framework.php - $container->setParameter('env(TRUSTED_HOSTS)', '10.0.0.1,10.0.0.2'); - $container->loadFromExtension('framework', [ - 'trusted_hosts' => '%env(csv:TRUSTED_HOSTS)%', - ]); + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container, FrameworkConfig $framework) { + $container->setParameter('env(TRUSTED_HOSTS)', '10.0.0.1,10.0.0.2'); + $framework->trustedHosts(env('TRUSTED_HOSTS')->csv()); + }; + +``env(shuffle:FOO)`` + Randomly shuffles values of the ``FOO`` env var, which must be an array. + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + parameters: + env(REDIS_NODES): "127.0.0.1:6380,127.0.0.1:6381" + services: + RedisCluster: + class: RedisCluster + arguments: [null, "%env(shuffle:csv:REDIS_NODES)%"] + + .. code-block:: xml + + + + + + + redis://127.0.0.1:6380,redis://127.0.0.1:6381 + + + + + null + %env(shuffle:csv:REDIS_NODES)% + + + + + .. code-block:: php + + // config/services.php + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + return static function (ContainerConfigurator $configurator): void { + $container = $configurator->services() + ->set(\RedisCluster::class, \RedisCluster::class)->args([null, '%env(shuffle:csv:REDIS_NODES)%']); + }; + + .. versionadded:: 6.2 + + The ``env(shuffle:...)`` env var processor was introduced in Symfony 6.2. ``env(file:FOO)`` Returns the contents of a file whose path is the value of the ``FOO`` env var: @@ -397,10 +513,6 @@ Symfony provides the following env var processors: 'auth' => '%env(require:PHP_FILE)%', ]); - .. versionadded:: 4.3 - - The ``require`` processor was introduced in Symfony 4.3. - ``env(trim:FOO)`` Trims the content of ``FOO`` env var, removing whitespaces from the beginning and end of the string. This is especially useful in combination with the @@ -443,10 +555,6 @@ Symfony provides the following env var processors: 'auth' => '%env(trim:file:AUTH_FILE)%', ]); - .. versionadded:: 4.3 - - The ``trim`` processor was introduced in Symfony 4.3. - ``env(key:FOO:BAR)`` Retrieves the value associated with the key ``FOO`` from the array whose contents are stored in the ``BAR`` env var: @@ -528,10 +636,6 @@ Symfony provides the following env var processors: When the fallback parameter is omitted (e.g. ``env(default::API_KEY)``), then the returned value is ``null``. - .. versionadded:: 4.3 - - The ``default`` processor was introduced in Symfony 4.3. - ``env(url:FOO)`` Parses an absolute URL and returns its components as an associative array. @@ -601,10 +705,6 @@ Symfony provides the following env var processors: In order to ease extraction of the resource from the URL, the leading ``/`` is trimmed from the ``path`` component. - .. versionadded:: 4.3 - - The ``url`` processor was introduced in Symfony 4.3. - ``env(query_string:FOO)`` Parses the query string part of the given URL and returns its components as an associative array. @@ -651,9 +751,52 @@ Symfony provides the following env var processors: ], ]); - .. versionadded:: 4.3 +``env(enum:FooEnum:BAR)`` + Tries to convert an environment variable to an actual ``\BackedEnum`` value. + This processor takes the fully qualified name of the ``\BackedEnum`` as an argument. + + .. code-block:: php + + # App\Enum\Environment + enum Environment: string + { + case Development = 'dev'; + case Production = 'prod'; + } + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + typed_env: '%env(enum:App\Enum\Environment:APP_ENV)%' + + .. code-block:: xml + + + + + + + %env(enum:App\Enum\Environment:APP_ENV)% + + + + .. code-block:: php + + // config/services.php + $container->setParameter('typed_env', '%env(enum:App\Enum\Environment:APP_ENV)%'); + + .. versionadded:: 6.2 - The ``query_string`` processor was introduced in Symfony 4.3. + The ``env(enum:...)`` env var processor was introduced in Symfony 6.2. It is also possible to combine any number of processors: @@ -717,7 +860,7 @@ create a class that implements class LowercasingEnvVarProcessor implements EnvVarProcessorInterface { - public function getEnv($prefix, $name, \Closure $getEnv) + public function getEnv(string $prefix, string $name, \Closure $getEnv) { $env = $getEnv($name); diff --git a/configuration/front_controllers_and_kernel.rst b/configuration/front_controllers_and_kernel.rst index b7b70456cb7..b1a6cf234d3 100644 --- a/configuration/front_controllers_and_kernel.rst +++ b/configuration/front_controllers_and_kernel.rst @@ -190,10 +190,13 @@ parameter used, for example, to turn Twig's debug mode on: .. code-block:: php - $container->loadFromExtension('twig', [ - 'debug' => '%kernel.debug%', + // config/packages/twig.php + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig) { // ... - ]); + $twig->debug('%kernel.debug%'); + }; The Environments ---------------- @@ -244,17 +247,16 @@ the directory of the environment you're using (most commonly ``dev/`` while developing and debugging). While it can vary, the ``var/cache/dev/`` directory includes the following: -``srcApp_KernelDevDebugContainer.php`` +``App_KernelDevDebugContainer.php`` The cached "service container" that represents the cached application configuration. -``UrlGenerator.php`` - The PHP class generated from the routing configuration and used when - generating URLs. +``url_generating_routes.php`` + The cached routing configuration used when generating URLs. -``UrlMatcher.php`` - The PHP class used for route matching - look here to see the compiled regular - expression logic used to match incoming URLs to different routes. +``url_matching_routes.php`` + The cached configuration used for route matching - look here to see the compiled + regular expression logic used to match incoming URLs to different routes. ``twig/`` This directory contains all the cached Twig templates. diff --git a/configuration/micro_kernel_trait.rst b/configuration/micro_kernel_trait.rst index 1d37d9843cb..3d479829a94 100644 --- a/configuration/micro_kernel_trait.rst +++ b/configuration/micro_kernel_trait.rst @@ -24,12 +24,11 @@ Next, create an ``index.php`` file that defines the kernel class and runs it:: // index.php use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; - use Symfony\Component\Config\Loader\LoaderInterface; - use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Kernel as BaseKernel; - use Symfony\Component\Routing\RouteCollectionBuilder; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; require __DIR__.'/vendor/autoload.php'; @@ -37,29 +36,27 @@ Next, create an ``index.php`` file that defines the kernel class and runs it:: { use MicroKernelTrait; - public function registerBundles() + public function registerBundles(): array { return [ - new Symfony\Bundle\FrameworkBundle\FrameworkBundle() + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), ]; } - protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader) + protected function configureContainer(ContainerConfigurator $c): void { // PHP equivalent of config/packages/framework.yaml - $c->loadFromExtension('framework', [ + $c->extension('framework', [ 'secret' => 'S0ME_SECRET' ]); } - protected function configureRoutes(RouteCollectionBuilder $routes) + protected function configureRoutes(RoutingConfigurator $routes): void { - // kernel is a service that points to this class - // optional 3rd argument is the route name - $routes->add('/random/{limit}', 'kernel::randomNumber'); + $routes->add('random_number', '/random/{limit}')->controller([$this, 'randomNumber']); } - public function randomNumber($limit) + public function randomNumber(int $limit): JsonResponse { return new JsonResponse([ 'number' => random_int(0, $limit), @@ -91,15 +88,15 @@ that define your bundles, your services and your routes: **registerBundles()** This is the same ``registerBundles()`` that you see in a normal kernel. -**configureContainer(ContainerBuilder $c, LoaderInterface $loader)** +**configureContainer(ContainerConfigurator $c)** This method builds and configures the container. In practice, you will use - ``loadFromExtension`` to configure different bundles (this is the equivalent + ``extension()`` to configure different bundles (this is the equivalent of what you see in a normal ``config/packages/*`` file). You can also register services directly in PHP or load external configuration files (shown below). -**configureRoutes(RouteCollectionBuilder $routes)** +**configureRoutes(RoutingConfigurator $routes)** Your job in this method is to add routes to the application. The - ``RouteCollectionBuilder`` has methods that make adding routes in PHP more + ``RoutingConfigurator`` has methods that make adding routes in PHP more fun. You can also load external routing files (shown below). Adding Interfaces to "Micro" Kernel @@ -144,7 +141,7 @@ Advanced Example: Twig, Annotations and the Web Debug Toolbar ------------------------------------------------------------- The purpose of the ``MicroKernelTrait`` is *not* to have a single-file application. -Instead, its goal to give you the power to choose your bundles and structure. +Instead, its goal is to give you the power to choose your bundles and structure. First, you'll probably want to put your PHP classes in an ``src/`` directory. Configure your ``composer.json`` file to load from there: @@ -164,70 +161,84 @@ your ``composer.json`` file to load from there: Then, run ``composer dump-autoload`` to dump your new autoload config. -Now, suppose you want to use Twig and load routes via annotations. Instead of -putting *everything* in ``index.php``, create a new ``src/Kernel.php`` to -hold the kernel. Now it looks like this:: +Now, suppose you want to define a custom configuration for your app, +use Twig and load routes via annotations. Instead of putting *everything* +in ``index.php``, create a new ``src/Kernel.php`` to hold the kernel. +Now it looks like this:: // src/Kernel.php namespace App; + use App\DependencyInjection\AppExtension; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; - use Symfony\Component\Config\Loader\LoaderInterface; - use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\HttpKernel\Kernel as BaseKernel; - use Symfony\Component\Routing\RouteCollectionBuilder; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; class Kernel extends BaseKernel { use MicroKernelTrait; - public function registerBundles() + public function registerBundles(): array { $bundles = [ new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(), new \Symfony\Bundle\TwigBundle\TwigBundle(), ]; - if ($this->getEnvironment() == 'dev') { - $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); + if ('dev' === $this->getEnvironment()) { + $bundles[] = new \Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); } return $bundles; } - protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader) + protected function build(ContainerBuilder $container) + { + $container->registerExtension(new AppExtension()); + } + + protected function configureContainer(ContainerConfigurator $c): void { - $loader->load(__DIR__.'/../config/framework.yaml'); + $c->import(__DIR__.'/../config/framework.yaml'); + + // register all classes in /src/ as service + $c->services() + ->load('App\\', __DIR__.'/*') + ->autowire() + ->autoconfigure() + ; // configure WebProfilerBundle only if the bundle is enabled if (isset($this->bundles['WebProfilerBundle'])) { - $c->loadFromExtension('web_profiler', [ + $c->extension('web_profiler', [ 'toolbar' => true, 'intercept_redirects' => false, ]); } } - protected function configureRoutes(RouteCollectionBuilder $routes) + protected function configureRoutes(RoutingConfigurator $routes): void { // import the WebProfilerRoutes, only if the bundle is enabled if (isset($this->bundles['WebProfilerBundle'])) { - $routes->import('@WebProfilerBundle/Resources/config/routing/wdt.xml', '/_wdt'); - $routes->import('@WebProfilerBundle/Resources/config/routing/profiler.xml', '/_profiler'); + $routes->import('@WebProfilerBundle/Resources/config/routing/wdt.xml')->prefix('/_wdt'); + $routes->import('@WebProfilerBundle/Resources/config/routing/profiler.xml')->prefix('/_profiler'); } - // load the annotation routes - $routes->import(__DIR__.'/../src/Controller/', '/', 'annotation'); + // load the routes defined as PHP attributes + // (use 'annotation' as the second argument if you define routes as annotations) + $routes->import(__DIR__.'/Controller/', 'attribute'); } // optional, to use the standard Symfony cache directory - public function getCacheDir() + public function getCacheDir(): string { return __DIR__.'/../var/cache/'.$this->getEnvironment(); } // optional, to use the standard Symfony logs directory - public function getLogDir() + public function getLogDir(): string { return __DIR__.'/../var/log'; } @@ -239,6 +250,39 @@ Before continuing, run this command to add support for the new dependencies: $ composer require symfony/yaml symfony/twig-bundle symfony/web-profiler-bundle doctrine/annotations +Next, create a new extension class that defines your app configuration and +add a service conditionally based on the ``foo`` value:: + + // src/DependencyInjection/AppExtension.php + namespace App\DependencyInjection; + + use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Extension\AbstractExtension; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + class AppExtension extends AbstractExtension + { + public function configure(DefinitionConfigurator $definition): void + { + $definition->rootNode() + ->children() + ->booleanNode('foo')->defaultTrue()->end() + ->end(); + } + + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + if ($config['foo']) { + $builder->register('foo_service', \stdClass::class); + } + } + } + +.. versionadded:: 6.1 + + The ``AbstractExtension`` class was introduced in Symfony 6.1. + Unlike the previous kernel, this loads an external ``config/framework.yaml`` file, because the configuration started to get bigger: @@ -269,12 +313,15 @@ because the configuration started to get bigger: .. code-block:: php // config/framework.php - $container->loadFromExtension('framework', [ - 'secret' => 'S0ME_SECRET', - 'profiler' => [ - 'only_exceptions' => false, - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework + ->secret('SOME_SECRET') + ->profiler() + ->onlyExceptions(false) + ; + }; This also loads annotation routes from an ``src/Controller/`` directory, which has one file in it:: @@ -283,14 +330,13 @@ has one file in it:: namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class MicroController extends AbstractController { - /** - * @Route("/random/{limit}") - */ - public function randomNumber($limit) + #[Route('/random/{limit}')] + public function randomNumber(int $limit): Response { $number = random_int(0, $limit); @@ -365,7 +411,6 @@ As before you can use the :doc:`Symfony Local Web Server .. code-block:: terminal - cd public/ $ symfony server:start Then visit the page in your browser: http://localhost:8000/random/10 diff --git a/configuration/multiple_kernels.rst b/configuration/multiple_kernels.rst index 029b4c1e5fb..9c24633106a 100644 --- a/configuration/multiple_kernels.rst +++ b/configuration/multiple_kernels.rst @@ -82,25 +82,15 @@ Kernel. Be sure to also change the location of the cache, logs and configuration files so they don't collide with the files from ``src/Kernel.php``:: // src/ApiKernel.php - use Symfony\Component\Config\Loader\LoaderInterface; - use Symfony\Component\DependencyInjection\ContainerBuilder; - use Symfony\Component\HttpKernel\Kernel; + use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Kernel as BaseKernel; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; class ApiKernel extends Kernel { use MicroKernelTrait; - public function registerBundles() - { - // load only the bundles strictly needed for the API - $contents = require $this->getProjectDir().'/config/api_bundles.php'; - foreach ($contents as $class => $envs) { - if ($envs[$this->environment] ?? $envs['all'] ?? false) { - yield new $class(); - } - } - } - public function getProjectDir(): string { return \dirname(__DIR__); @@ -108,7 +98,7 @@ files so they don't collide with the files from ``src/Kernel.php``:: public function getCacheDir(): string { - return $this->getProjectDir().'/var/cache/api/'.$this->getEnvironment(); + return $this->getProjectDir().'/var/cache/api/'.$this->environment; } public function getLogDir(): string @@ -116,23 +106,33 @@ files so they don't collide with the files from ``src/Kernel.php``:: return $this->getProjectDir().'/var/log/api'; } - public function configureContainer(ContainerBuilder $container, LoaderInterface $loader) + protected function configureContainer(ContainerConfigurator $container): void { - $container->addResource(new FileResource($this->getProjectDir().'/config/api_bundles.php')); - $container->setParameter('container.dumper.inline_factories', true); - $confDir = $this->getProjectDir().'/config/api'; - - $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); + $container->import('../config/api/{packages}/*.yaml'); + $container->import('../config/api/{packages}/'.$this->environment.'/*.yaml'); + + if (is_file(\dirname(__DIR__).'/config/api/services.yaml')) { + $container->import('../config/api/services.yaml'); + $container->import('../config/api/{services}_'.$this->environment.'.yaml'); + } else { + $container->import('../config/api/{services}.php'); + } } - protected function configureRoutes(RouteCollectionBuilder $routes): void + protected function configureRoutes(RoutingConfigurator $routes): void { - $confDir = $this->getProjectDir().'/config/api'; + $routes->import('../config/api/{routes}/'.$this->environment.'/*.yaml'); + $routes->import('../config/api/{routes}/*.yaml'); // ... load only the config routes strictly needed for the API } + + // If you need to run some logic to decide which bundles to load, + // you might prefer to use the registerBundles() method instead + private function getBundlesPath(): string + { + // load only the bundles strictly needed for the API + return $this->getProjectDir().'/config/api_bundles.php'; + } } Step 3) Define the Kernel Configuration diff --git a/configuration/override_dir_structure.rst b/configuration/override_dir_structure.rst index a1af58ba5db..73f65c9171f 100644 --- a/configuration/override_dir_structure.rst +++ b/configuration/override_dir_structure.rst @@ -25,7 +25,58 @@ override it to create your own structure: │ ├─ cache/ │ ├─ log/ │ └─ ... - └─ vendor/ + ├─ vendor/ + └─ .env + +.. _override-env-dir: + +Override the Environment (DotEnv) Files Directory +------------------------------------------------- + +By default, the :ref:`.env configuration file ` is located at +the root directory of the project. If you store it in a different location, +define the ``runtime.dotenv_path`` option in the ``composer.json`` file: + +.. code-block:: json + + { + "...": "...", + "extra": { + "...": "...", + "runtime": { + "dotenv_path": "my/custom/path/to/.env" + } + } + } + +Then, update your Composer files (running ``composer update``, for instance), +so that the ``vendor/autoload_runtime.php`` files gets regenerated with the new +``.env`` path. + +You can also set up different ``.env`` paths for your console and web server +calls. Edit the ``public/index.php`` and/or ``bin/console`` files to define the +new file path. + +Console script:: + + // bin/console + + // ... + $_SERVER['APP_RUNTIME_OPTIONS']['dotenv_path'] = 'some/custom/path/to/.env'; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + // ... + +Web front-controller:: + + // public/index.php + + // ... + $_SERVER['APP_RUNTIME_OPTIONS']['dotenv_path'] = 'another/custom/path/to/.env'; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + // ... + .. _override-config-dir: @@ -41,8 +92,8 @@ at your project root directory. Override the Cache Directory ---------------------------- -You can change the default cache directory by overriding the ``getCacheDir()`` -method in the ``Kernel`` class of your application:: +Changing the cache directory can be achieved by overriding the +``getCacheDir()`` method in the ``Kernel`` class of your application:: // src/Kernel.php @@ -61,6 +112,9 @@ In this code, ``$this->environment`` is the current environment (i.e. ``dev``). In this case you have changed the location of the cache directory to ``var/{environment}/cache/``. +You can also change the cache directory defining an environment variable named +``APP_CACHE_DIR`` whose value is the full path of the cache folder. + .. caution:: You should keep the cache directory different for each environment, @@ -73,9 +127,11 @@ In this case you have changed the location of the cache directory to Override the Log Directory -------------------------- -Overriding the ``var/log/`` directory is the same as overriding the ``var/cache/`` -directory. The only difference is that you need to override the ``getLogDir()`` -method:: +Overriding the ``var/log/`` directory is almost the same as overriding the +``var/cache/`` directory. + +You can do it overriding the ``getLogDir()`` method in the ``Kernel`` class of +your application:: // src/Kernel.php @@ -92,6 +148,9 @@ method:: Here you have changed the location of the directory to ``var/{environment}/log/``. +You can also change the log directory defining an environment variable named +``APP_LOG_DIR`` whose value is the full path of the log folder. + .. _override-templates-dir: Override the Templates Directory @@ -132,9 +191,11 @@ for multiple directories): .. code-block:: php // config/packages/twig.php - $container->loadFromExtension('twig', [ - 'default_path' => '%kernel.project_dir%/resources/views', - ]); + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig) { + $twig->defaultPath('%kernel.project_dir%/resources/views'); + }; Override the Translations Directory ----------------------------------- @@ -176,11 +237,13 @@ configuration option to define your own translations directory (use :ref:`framew .. code-block:: php // config/packages/translation.php - $container->loadFromExtension('framework', [ - 'translator' => [ - 'default_path' => '%kernel.project_dir%/i18n', - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->translator() + ->defaultPath('%kernel.project_dir%/i18n') + ; + }; .. _override-web-dir: .. _override-the-web-directory: diff --git a/configuration/secrets.rst b/configuration/secrets.rst index 845a2106af7..29c7dbed4ad 100644 --- a/configuration/secrets.rst +++ b/configuration/secrets.rst @@ -4,10 +4,6 @@ How to Keep Sensitive Information Secret ======================================== -.. versionadded:: 4.4 - - The Secrets management was introduced in Symfony 4.4. - :ref:`Environment variables ` are the best way to store configuration that depends on where the application is run - for example, some API key that might be set to one value while developing locally and another value on production. @@ -18,10 +14,7 @@ store them by using Symfony's secrets management system - sometimes called a .. note:: - The Secrets system requires the sodium PHP extension that is bundled - with PHP 7.2. If you're using an earlier PHP version, you can - install the `libsodium`_ PHP extension or use the - `paragonie/sodium_compat`_ package. + The Secrets system requires the Sodium PHP extension. .. _secrets-generate-keys: @@ -52,7 +45,7 @@ running: .. code-block:: terminal - $ php bin/console secrets:generate-keys --env=prod + $ APP_RUNTIME_ENV=prod php bin/console secrets:generate-keys This will generate ``config/secrets/prod/prod.encrypt.public.php`` and ``config/secrets/prod/prod.decrypt.private.php``. @@ -82,7 +75,7 @@ Suppose you want to store your database password as a secret. By using the $ php bin/console secrets:set DATABASE_PASSWORD # set your production value - $ php bin/console secrets:set DATABASE_PASSWORD --env=prod + $ APP_RUNTIME_ENV=prod php bin/console secrets:set DATABASE_PASSWORD This will create a new file for the secret in ``config/secrets/dev`` and another in ``config/secrets/prod``. You can also set the secret in a few other ways: @@ -147,11 +140,14 @@ If you stored a ``DATABASE_PASSWORD`` secret, you can reference it by: .. code-block:: php // config/packages/doctrine.php - $container->loadFromExtension('doctrine', [ - 'dbal' => [ - 'password' => '%env(DATABASE_PASSWORD)%', - ], - ]); + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine) { + $doctrine->dbal() + ->connection('default') + ->password(env('DATABASE_PASSWORD')) + ; + }; The actual value will be resolved at runtime: container compilation and cache warmup don't need the **decryption key**. @@ -262,7 +258,7 @@ manually store this file somewhere and deploy it. There are 2 ways to do that: .. code-block:: terminal - $ php bin/console secrets:decrypt-to-local --force --env=prod + $ APP_RUNTIME_ENV=prod php bin/console secrets:decrypt-to-local --force This will write all the decrypted secrets into the ``.env.prod.local`` file. After doing this, the decryption key does *not* need to remain on the server(s). @@ -314,14 +310,12 @@ The secrets system is enabled by default and some of its behavior can be configu .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'secrets' => [ - // 'vault_directory' => '%kernel.project_dir%/config/secrets/%kernel.environment%', - // 'local_dotenv_file' => '%kernel.project_dir%/.env.%kernel.environment%.local', - // 'decryption_env_var' => 'base64:default::SYMFONY_DECRYPTION_SECRET', - ], - ]); - - -.. _`libsodium`: https://pecl.php.net/package/libsodium -.. _`paragonie/sodium_compat`: https://github.com/paragonie/sodium_compat + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->secrets() + // ->vaultDirectory('%kernel.project_dir%/config/secrets/%kernel.environment%') + // ->localDotenvFile('%kernel.project_dir%/.env.%kernel.environment%.local') + // ->decryptionEnvVar('base64:default::SYMFONY_DECRYPTION_SECRET') + ; + }; diff --git a/console.rst b/console.rst index f67dfb71f5d..61418f6f039 100644 --- a/console.rst +++ b/console.rst @@ -9,15 +9,93 @@ The Symfony framework provides lots of commands through the ``bin/console`` scri created with the :doc:`Console component `. You can also use it to create your own commands. -The Console: APP_ENV & APP_DEBUG ---------------------------------- +Running Commands +---------------- + +Each Symfony application comes with a large set of commands. You can use +the ``list`` command to view all available commands in the application: + +.. code-block:: terminal + + $ php bin/console list + ... + + Available commands: + about Display information about the current project + completion Dump the shell completion script + help Display help for a command + list List commands + assets + assets:install Install bundle's web assets under a public directory + cache + cache:clear Clear the cache + ... + +If you find the command you need, you can run it with the ``--help`` option +to view the command's documentation: + +.. code-block:: terminal + + $ php bin/console assets:install --help + +APP_ENV & APP_DEBUG +~~~~~~~~~~~~~~~~~~~ Console commands run in the :ref:`environment ` defined in the ``APP_ENV`` variable of the ``.env`` file, which is ``dev`` by default. It also reads the ``APP_DEBUG`` value to turn "debug" mode on or off (it defaults to ``1``, which is on). To run the command in another environment or debug mode, edit the value of ``APP_ENV`` -and ``APP_DEBUG``. +and ``APP_DEBUG``. You can also define this env vars when running the +command, for instance: + +.. code-block:: terminal + + # clears the cache for the prod environment + $ APP_ENV=prod php bin/console cache:clear + +.. _console-completion-setup: + +Console Completion +~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 6.1 + + Console completion for Fish was introduced in Symfony 6.1. + +.. versionadded:: 6.2 + + Console completion for Zsh was introduced in Symfony 6.2. + +If you are using the Bash, Zsh or Fish shell, you can install Symfony's +completion script to get auto completion when typing commands in the +terminal. All commands support name and option completion, and some can +even complete values. + +.. image:: /_images/components/console/completion.gif + +First, you have to install the completion script *once*. Run +``bin/console completion --help`` for the installation instructions for +your shell. + +.. note:: + + When using Bash, make sure you installed and setup the "bash completion" + package for your OS (typically named ``bash-completion``). + +After installing and restarting your terminal, you're all set to use +completion (by default, by pressing the Tab key). + +.. tip:: + + Many PHP tools are built using the Symfony Console component (e.g. + Composer, PHPstan and Behat). If they are using version 5.4 or higher, + you can also install their completion script to enable console completion: + + .. code-block:: terminal + + $ php vendor/bin/phpstan completion --help + $ composer completion --help Creating a Command ------------------ @@ -29,54 +107,73 @@ want a command to create a user:: // src/Command/CreateUserCommand.php namespace App\Command; + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; + // the name of the command is what users type after "php bin/console" + #[AsCommand(name: 'app:create-user')] class CreateUserCommand extends Command { - // the name of the command (the part after "bin/console") - protected static $defaultName = 'app:create-user'; - - protected function configure(): void - { - // ... - } - protected function execute(InputInterface $input, OutputInterface $output): int { // ... put here the code to create the user // this method must return an integer number with the "exit status code" - // of the command. + // of the command. You can also use these constants to make code more readable // return this if there was no problem running the command - return 0; + // (it's equivalent to returning int(0)) + return Command::SUCCESS; // or return this if some error happened during the execution - // return 1; + // (it's equivalent to returning int(1)) + // return Command::FAILURE; + + // or return this to indicate incorrect command usage; e.g. invalid options + // or missing arguments (it's equivalent to returning int(2)) + // return Command::INVALID } } Configuring the Command ------------------------ +~~~~~~~~~~~~~~~~~~~~~~~ You can optionally define a description, help message and the -:doc:`input options and arguments `:: +:doc:`input options and arguments ` by overriding the +``configure()`` method:: + + // src/Command/CreateUserCommand.php // ... - protected function configure(): void + class CreateUserCommand extends Command { - $this - // the short description shown while running "php bin/console list" - ->setDescription('Creates a new user.') + // the command description shown when running "php bin/console list" + protected static $defaultDescription = 'Creates a new user.'; - // the full command description shown when running the command with - // the "--help" option - ->setHelp('This command allows you to create a user...') - ; + // ... + protected function configure(): void + { + $this + // the command help shown when running the command with the "--help" option + ->setHelp('This command allows you to create a user...') + ; + } } +.. tip:: + + Defining the ``$defaultDescription`` static property instead of using the + ``setDescription()`` method allows to get the command description without + instantiating its class. This makes the ``php bin/console list`` command run + much faster. + + If you want to always run the ``list`` command fast, add the ``--short`` option + to it (``php bin/console list --short``). This will avoid instantiating command + classes, but it won't show any description for commands that use the + ``setDescription()`` method instead of the static property. + The ``configure()`` method is called automatically at the end of the command constructor. If your command defines its own constructor, set the properties first and then call to the parent constructor, to make those properties @@ -110,15 +207,37 @@ available in the ``configure()`` method:: } Registering the Command ------------------------ +~~~~~~~~~~~~~~~~~~~~~~~ -Symfony commands must be registered as services and :doc:`tagged ` -with the ``console.command`` tag. If you're using the +In PHP 8 and newer versions, you can register the command by adding the +``AsCommand`` attribute to it:: + + // src/Command/CreateUserCommand.php + namespace App\Command; + + use Symfony\Component\Console\Attribute\AsCommand; + use Symfony\Component\Console\Command\Command; + + // the "name" and "description" arguments of AsCommand replace the + // static $defaultName and $defaultDescription properties + #[AsCommand( + name: 'app:create-user', + description: 'Creates a new user.', + hidden: false, + aliases: ['app:add-user'] + )] + class CreateUserCommand extends Command + { + // ... + } + +If you can't use PHP attributes, register the command as a service and +:doc:`tag it ` with the ``console.command`` tag. If you're using the :ref:`default services.yaml configuration `, this is already done for you, thanks to :ref:`autoconfiguration `. -Executing the Command ---------------------- +Running the Command +~~~~~~~~~~~~~~~~~~~ After configuring and registering the command, you can run it in the terminal: @@ -156,7 +275,7 @@ the console:: $output->write('You are about to '); $output->write('create a user.'); - return 0; + return Command::SUCCESS; } Now, try executing the command: @@ -215,7 +334,13 @@ method, which returns an instance of $section1->clear(2); // Output is now completely empty! - return 0; + // setting the max height of a section will make new lines replace the old ones + $section1->setMaxHeight(2); + $section1->writeln('Line1'); + $section1->writeln('Line2'); + $section1->writeln('Line3'); + + return Command::SUCCESS; } } @@ -223,6 +348,10 @@ method, which returns an instance of A new line is appended automatically when displaying information in a section. +.. versionadded:: 6.2 + + The feature to limit the height of a console section was introduced in Symfony 6.2. + Output sections let you manipulate the Console output in advanced ways, such as :ref:`displaying multiple progress bars ` which are updated independently and :ref:`appending rows to tables ` @@ -257,7 +386,7 @@ Use input options or arguments to pass information to the command:: // retrieve the argument value using getArgument() $output->writeln('Username: '.$input->getArgument('username')); - return 0; + return Command::SUCCESS; } Now, you can pass the username to the command: @@ -308,7 +437,7 @@ as a service, you can use normal dependency injection. Imagine you have a $output->writeln('User successfully generated!'); - return 0; + return Command::SUCCESS; } } @@ -332,14 +461,9 @@ command: :method:`Symfony\\Component\\Console\\Command\\Command::execute` *(required)* This method is executed after ``interact()`` and ``initialize()``. - It contains the logic you want the command to execute and it should + It contains the logic you want the command to execute and it must return an integer which will be used as the command `exit status`_. - .. deprecated:: 4.4 - - Not returning an integer with the exit status as the result of - ``execute()`` is deprecated since Symfony 4.4. - .. _console-testing-commands: Testing Commands @@ -376,6 +500,8 @@ console:: // e.g: '--some-option' => ['option_value'], ]); + $commandTester->assertCommandIsSuccessful(); + // the output of the command in the console $output = $commandTester->getDisplay(); $this->assertStringContainsString('Username: Wouter', $output); @@ -384,6 +510,9 @@ console:: } } +If you are using a :doc:`single-command application `, +call ``setAutoExit(false)`` on it to get the command result in ``CommandTester``. + .. tip:: You can also test a whole console application by using @@ -402,8 +531,17 @@ console:: $application = new Application(); $application->setAutoExit(false); - + $tester = new ApplicationTester($application); + + +.. caution:: + + When testing ``InputOption::VALUE_NONE`` command options, you must pass an + empty value to them:: + + $commandTester = new CommandTester($command); + $commandTester->execute(['--some-option' => '']); .. note:: @@ -438,5 +576,6 @@ tools capable of helping you with different tasks: * :doc:`/components/console/helpers/table`: displays tabular data as a table * :doc:`/components/console/helpers/debug_formatter`: provides functions to output debug information when running an external program +* :doc:`/components/console/helpers/cursor`: allows to manipulate the cursor in the terminal .. _`exit status`: https://en.wikipedia.org/wiki/Exit_status diff --git a/console/coloring.rst b/console/coloring.rst index d3b2d6f67d3..a481b7650ff 100644 --- a/console/coloring.rst +++ b/console/coloring.rst @@ -40,13 +40,25 @@ It is possible to define your own styles using the use Symfony\Component\Console\Formatter\OutputFormatterStyle; // ... - $outputStyle = new OutputFormatterStyle('red', 'yellow', ['bold', 'blink']); + $outputStyle = new OutputFormatterStyle('red', '#ff0', ['bold', 'blink']); $output->getFormatter()->setStyle('fire', $outputStyle); $output->writeln('foo'); -Available foreground and background colors are: ``black``, ``red``, ``green``, -``yellow``, ``blue``, ``magenta``, ``cyan`` and ``white``. +Any hex color is supported for foreground and background colors. Besides that, these named colors are supported: +``black``, ``red``, ``green``, ``yellow``, ``blue``, ``magenta``, ``cyan``, ``white``, +``gray``, ``bright-red``, ``bright-green``, ``bright-yellow``, ``bright-blue``, +``bright-magenta``, ``bright-cyan`` and ``bright-white``. + +.. note:: + + If the terminal doesn't support true colors, the given color is replaced by + the nearest color depending on the terminal capabilities. E.g. ``#c0392b`` is + degraded to ``#d75f5f`` in 256-color terminals and to ``red`` in 8-color terminals. + + .. versionadded:: 6.2 + + The support for 256-color terminals was introduced in Symfony 6.2. And available options are: ``bold``, ``underscore``, ``blink``, ``reverse`` (enables the "reverse video" mode where the background and foreground colors @@ -56,9 +68,12 @@ commonly used when asking the user to type sensitive information). You can also set these colors and options directly inside the tag name:: - // green text + // using named colors $output->writeln('foo'); + // using hexadecimal colors + $output->writeln('foo'); + // black text on a cyan background $output->writeln('foo'); @@ -77,10 +92,6 @@ You can also set these colors and options directly inside the tag name:: Displaying Clickable Links ~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 4.3 - - The feature to display clickable links was introduced in Symfony 4.3. - Commands can use the special ```` tag to display links similar to the ```` elements of web pages:: diff --git a/console/commands_as_services.rst b/console/commands_as_services.rst index 794ec8f46cb..d279c762ec6 100644 --- a/console/commands_as_services.rst +++ b/console/commands_as_services.rst @@ -18,13 +18,14 @@ For example, suppose you want to log something from within your command:: namespace App\Command; use Psr\Log\LoggerInterface; + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; + #[AsCommand(name: 'app:sunshine')] class SunshineCommand extends Command { - protected static $defaultName = 'app:sunshine'; private $logger; public function __construct(LoggerInterface $logger) @@ -46,7 +47,7 @@ For example, suppose you want to log something from within your command:: $this->logger->info('Waking up the sun'); // ... - return 0; + return Command::SUCCESS; } } @@ -63,24 +64,20 @@ command and start logging. work (e.g. making database queries), as that code will be run, even if you're using the console to execute a different command. -.. note:: - - In previous Symfony versions, you could make the command class extend from - :class:`Symfony\\Bundle\\FrameworkBundle\\Command\\ContainerAwareCommand` to - get services via ``$this->getContainer()->get('SERVICE_ID')``. This is - deprecated in Symfony 4.2 and it won't work in future Symfony versions. - .. _console-command-service-lazy-loading: Lazy Loading ------------ -To make your command lazily loaded, either define its ``$defaultName`` static property:: +To make your command lazily loaded, either define its name using the PHP +``AsCommand`` attribute:: + use Symfony\Component\Console\Attribute\AsCommand; + // ... + + #[AsCommand(name: 'app:sunshine')] class SunshineCommand extends Command { - protected static $defaultName = 'app:sunshine'; - // ... } diff --git a/console/hide_commands.rst b/console/hide_commands.rst index 2f9d2819873..44a69d09289 100644 --- a/console/hide_commands.rst +++ b/console/hide_commands.rst @@ -8,25 +8,19 @@ However, sometimes commands are not intended to be run by end-users; for example, commands for the legacy parts of the application, commands exclusively run through scheduled tasks, etc. -In those cases, you can define the command as **hidden** by setting the -``setHidden()`` method to ``true`` in the command configuration:: +In those cases, you can define the command as **hidden** by setting to ``true`` +the ``hidden`` property of the ``AsCommand`` attribute:: // src/Command/LegacyCommand.php namespace App\Command; + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; + #[AsCommand(name: 'app:legacy', hidden: true)] class LegacyCommand extends Command { - protected static $defaultName = 'app:legacy'; - - protected function configure(): void - { - $this - ->setHidden(true) - // ... - ; - } + // ... } Hidden commands behave the same as normal commands but they are no longer displayed diff --git a/console/input.rst b/console/input.rst index c48968c81fc..27c0c753ce8 100644 --- a/console/input.rst +++ b/console/input.rst @@ -53,7 +53,7 @@ You now have access to a ``last_name`` argument in your command:: $output->writeln($text.'!'); - return 0; + return Command::SUCCESS; } } @@ -206,7 +206,7 @@ separation at all (e.g. ``-i 5`` or ``-i5``). this situation, always place options after the command name, or avoid using a space to separate the option name from its value. -There are four option variants you can use: +There are five option variants you can use: ``InputOption::VALUE_IS_ARRAY`` This option accepts multiple values (e.g. ``--dir=/foo --dir=/bar``); @@ -224,6 +224,10 @@ There are four option variants you can use: This option may or may not have a value (e.g. ``--yell`` or ``--yell=loud``). +``InputOption::VALUE_NEGATABLE`` + Accept either the flag (e.g. ``--yell``) or its negation (e.g. + ``--no-yell``). + You need to combine ``VALUE_IS_ARRAY`` with ``VALUE_REQUIRED`` or ``VALUE_OPTIONAL`` like this:: @@ -307,4 +311,98 @@ The above code can be simplified as follows because ``false !== null``:: $yell = ($optionValue !== false); $yellLouder = ($optionValue === 'louder'); +Adding Argument/Option Value Completion +--------------------------------------- + +If :ref:`Console completion is installed `, +command and option names will be auto completed by the shell. However, you +can also implement value completion for the input in your commands. For +instance, you may want to complete all usernames from the database in the +``name`` argument of your greet command. + +To achieve this, use the 5th argument of ``addArgument()``/``addOption``:: + + // ... + use Symfony\Component\Console\Completion\CompletionInput; + use Symfony\Component\Console\Completion\CompletionSuggestions; + + class GreetCommand extends Command + { + // ... + protected function configure(): void + { + $this + ->addArgument( + 'names', + InputArgument::IS_ARRAY, + 'Who do you want to greet (separate multiple names with a space)?', + null, + function (CompletionInput $input) { + // the value the user already typed, e.g. when typing "app:greet Fa" before + // pressing Tab, this will contain "Fa" + $currentValue = $input->getCompletionValue(); + + // get the list of username names from somewhere (e.g. the database) + // you may use $currentValue to filter down the names + $availableUsernames = ...; + + // then suggested the usernames as values + return $availableUsernames; + } + ) + ; + } + } + +.. versionadded:: 6.1 + + The argument to ``addOption()``/``addArgument()`` was introduced in + Symfony 6.1. Prior to this version, you had to override the + ``complete()`` method of the command. + +That's all you need! Assuming users "Fabien" and "Fabrice" exist, pressing +tab after typing ``app:greet Fa`` will give you these names as a suggestion. + +.. tip:: + + The shell script is able to handle huge amounts of suggestions and will + automatically filter the suggested values based on the existing input + from the user. You do not have to implement any filter logic in the + command. + + You may use ``CompletionInput::getCompletionValue()`` to get the + current input if that helps improving performance (e.g. by reducing the + number of rows fetched from the database). + +Testing the Completion script +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Console component comes with a special +:class:`Symfony\\Component\\Console\\Tester\\CommandCompletionTester`` class +to help you unit test the completion logic:: + + // ... + use Symfony\Component\Console\Application; + + class GreetCommandTest extends TestCase + { + public function testComplete() + { + $application = new Application(); + $application->add(new GreetCommand()); + + // create a new tester with the greet command + $tester = new CommandCompletionTester($application->get('app:greet')); + + // complete the input without any existing input (the empty string represents + // the position of the cursor) + $suggestions = $tester->complete(['']); + $this->assertSame(['Fabien', 'Fabrice', 'Wouter'], $suggestions); + + // complete the input with "Fa" as input + $suggestions = $tester->complete(['Fa']); + $this->assertSame(['Fabien', 'Fabrice'], $suggestions); + } + } + .. _`docopt standard`: http://docopt.org/ diff --git a/console/lockable_trait.rst b/console/lockable_trait.rst index 98c94d82c57..02f635f5788 100644 --- a/console/lockable_trait.rst +++ b/console/lockable_trait.rst @@ -27,7 +27,7 @@ that adds two convenient methods to lock and release commands:: if (!$this->lock()) { $output->writeln('The command is already running in another process.'); - return 0; + return Command::SUCCESS; } // If you prefer to wait until the lock is released, use this: @@ -39,7 +39,7 @@ that adds two convenient methods to lock and release commands:: // automatically when the execution of the command ends $this->release(); - return 0; + return Command::SUCCESS; } } diff --git a/console/style.rst b/console/style.rst index 79a4971b2c8..16cb3e2bab0 100644 --- a/console/style.rst +++ b/console/style.rst @@ -152,10 +152,6 @@ Content Methods ] ); - .. versionadded:: 4.4 - - The ``horizontalTable()`` method was introduced in Symfony 4.4. - :method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::definitionList` It displays the given ``key => value`` pairs as a compact list of elements:: @@ -169,9 +165,10 @@ Content Methods ['foo4' => 'bar4'] ); - .. versionadded:: 4.4 - - The ``definitionList()`` method was introduced in Symfony 4.4. +:method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::createTable` + Creates an instance of :class:`Symfony\\Component\\Console\\Helper\\Table` + styled according to the Symfony Style Guide, which allows you to use + features such as dynamically appending rows. :method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::newLine` It displays a blank line in the command output. Although it may seem useful, @@ -251,6 +248,20 @@ Progress Bar Methods $io->progressFinish(); +:method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::progressIterate` + If your progress bar loops over an iterable collection, use the + ``progressIterate()`` helper:: + + $iterable = [1, 2]; + + foreach ($io->progressIterate($iterable) as $value) { + // ... do some work + } + +:method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::createProgressBar` + Creates an instance of :class:`Symfony\\Component\\Console\\Helper\\ProgressBar` + styled according to the Symfony Style Guide. + User Input Methods ~~~~~~~~~~~~~~~~~~ @@ -313,9 +324,26 @@ User Input Methods $io->choice('Select the queue to analyze', ['queue1', 'queue2', 'queue3'], 'queue1'); + Finally, you can allow users to select multiple choices. To do so, users must + separate each choice with a comma (e.g. typing ``1, 2`` will select choice 1 + and 2):: + + $io->choice('Select the queue to analyze', ['queue1', 'queue2', 'queue3'], multiSelect: true); + +.. versionadded:: 6.2 + + The ``multiSelect`` option of ``choice()`` was introduced in Symfony 6.2. + Result Methods ~~~~~~~~~~~~~~ +.. note:: + + If you print any URL it won't be broken/cut, it will be clickable - if the terminal provides it. If the "well + formatted output" is more important, you can switch it off:: + + $io->getOutputWrapper()->setAllowCutUrls(true); + :method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::success` It displays the given string or array of strings highlighted as a successful message (with a green background and the ``[OK]`` label). It's meant to be @@ -333,6 +361,23 @@ Result Methods 'Consectetur adipiscing elit', ]); +:method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::info` + It's similar to the ``success()`` method (the given string or array of strings + are displayed with a green background) but the ``[OK]`` label is not prefixed. + It's meant to be used once to display the final result of executing the given + command, without showing the result as a successful or failed one:: + + // use simple strings for short info messages + $io->info('Lorem ipsum dolor sit amet'); + + // ... + + // consider using arrays when displaying long info messages + $io->info([ + 'Lorem ipsum dolor sit amet', + 'Consectetur adipiscing elit', + ]); + :method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::warning` It displays the given string or array of strings highlighted as a warning message (with a red background and the ``[WARNING]`` label). It's meant to be @@ -367,6 +412,38 @@ Result Methods 'Consectetur adipiscing elit', ]); +Configuring the Default Styles +------------------------------ + +By default, Symfony Styles wrap all contents to avoid having lines of text that +are too long. The only exception is URLs, which are not wrapped, no matter how +long they are. This is done to enable clickable URLs in terminals that support them. + +If you prefer to wrap all contents, including URLs, use this method:: + + // src/Command/GreetCommand.php + namespace App\Command; + + // ... + use Symfony\Component\Console\Style\SymfonyStyle; + + class GreetCommand extends Command + { + // ... + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $io->getOutputWrapper()->setAllowCutUrls(true); + + // ... + } + } + +.. versionadded:: 6.2 + + The ``setAllowCutUrls()`` method was introduced in Symfony 6.2. + Defining your Own Styles ------------------------ diff --git a/contributing/code/conventions.rst b/contributing/code/conventions.rst index 7a41d20cc7c..cd1d87b4282 100644 --- a/contributing/code/conventions.rst +++ b/contributing/code/conventions.rst @@ -146,40 +146,35 @@ A feature is marked as deprecated by adding a ``@deprecated`` PHPDoc to relevant classes, methods, properties, ...:: /** - * @deprecated since Symfony 2.8. + * @deprecated since Symfony 5.1. */ The deprecation message must indicate the version in which the feature was deprecated, and whenever possible, how it was replaced:: /** - * @deprecated since Symfony 2.8, use Replacement instead. + * @deprecated since Symfony 5.1, use Replacement instead. */ When the replacement is in another namespace than the deprecated class, its FQCN must be used:: /** - * @deprecated since Symfony 2.8, use A\B\Replacement instead. + * @deprecated since Symfony 5.1, use A\B\Replacement instead. */ -A PHP ``E_USER_DEPRECATED`` error must also be triggered to help people with the migration:: +A deprecation must also be triggered to help people with the migration +(requires the ``symfony/deprecation-contracts`` package):: - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 2.8, use "%s" instead.', Deprecated::class, Replacement::class), E_USER_DEPRECATED); + trigger_deprecation('symfony/package-name', '5.1', 'The "%s" class is deprecated, use "%s" instead.', Deprecated::class, Replacement::class); -Without the `@-silencing operator`_, users would need to opt-out from deprecation -notices. Silencing swaps this behavior and allows users to opt-in when they are -ready to cope with them (by adding a custom error handler like the one used by -the Web Debug Toolbar or by the PHPUnit bridge). - -When deprecating a whole class the ``trigger_error()`` call should be placed -after the use declarations, like in this example from -`ServiceRouterLoader`_:: +When deprecating a whole class the ``trigger_deprecation()`` call should be placed +after the use declarations, like in this example from `ServiceRouterLoader`_:: namespace Symfony\Component\Routing\Loader\DependencyInjection; use Symfony\Component\Routing\Loader\ContainerLoader; - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ServiceRouterLoader::class, ContainerLoader::class), E_USER_DEPRECATED); + trigger_deprecation('symfony/routing', '4.4', 'The "%s" class is deprecated, use "%s" instead.', ServiceRouterLoader::class, ContainerLoader::class); /** * @deprecated since Symfony 4.4, use Symfony\Component\Routing\Loader\ContainerLoader instead. @@ -237,8 +232,6 @@ to the ``CHANGELOG.md`` file of the impacted component: This task is mandatory and must be done in the same pull request. -.. _`@-silencing operator`: https://www.php.net/manual/en/language.operators.errorcontrol.php - Naming Commands and Options --------------------------- diff --git a/contributing/code/pull_requests.rst b/contributing/code/pull_requests.rst index 4364190bcde..b63ea8135b4 100644 --- a/contributing/code/pull_requests.rst +++ b/contributing/code/pull_requests.rst @@ -31,7 +31,7 @@ Before working on Symfony, setup a friendly environment with the following software: * Git; -* PHP version 7.1.3 or above. +* PHP version 7.2.5 or above. Configure Git ~~~~~~~~~~~~~ @@ -153,7 +153,7 @@ topic branch: .. code-block:: terminal - $ git checkout -b BRANCH_NAME 5.x + $ git checkout -b BRANCH_NAME 6.1 Or, if you want to provide a bug fix for the ``4.4`` branch, first track the remote ``4.4`` branch locally: @@ -282,15 +282,15 @@ while to finish your changes): .. code-block:: terminal - $ git checkout 5.x + $ git checkout 6.1 $ git fetch upstream - $ git merge upstream/5.x + $ git merge upstream/6.1 $ git checkout BRANCH_NAME - $ git rebase 5.x + $ git rebase 6.1 .. tip:: - Replace ``5.x`` with the branch you selected previously (e.g. ``4.4``) + Replace ``6.1`` with the branch you selected previously (e.g. ``4.4``) if you are working on a bug fix. When doing the ``rebase`` command, you might have to fix merge conflicts. @@ -503,7 +503,7 @@ PR. Before re-submitting the PR, rebase with ``upstream/5.x`` or .. code-block:: terminal - $ git rebase -f upstream/5.x + $ git rebase -f upstream/6.1 $ git push --force origin BRANCH_NAME .. note:: diff --git a/contributing/code/reproducer.rst b/contributing/code/reproducer.rst index 771bd69eeac..6efae2a8ee8 100644 --- a/contributing/code/reproducer.rst +++ b/contributing/code/reproducer.rst @@ -65,8 +65,9 @@ to a route definition. Then, after creating your project: of controllers, actions, etc. as in your original application. #. Create a small controller and add your routing definition that shows the bug. #. Don't create or modify any other file. -#. Execute ``composer require symfony/web-server-bundle`` and use the ``server:run`` - command to browse to the new route and see if the bug appears or not. +#. Install the :doc:`local web server ` provided by Symfony + and use the ``symfony server:start`` command to browse to the new route and + see if the bug appears or not. #. If you can see the bug, you're done and you can already share the code with us. #. If you can't see the bug, you must keep making small changes. For example, if your original route was defined using XML, forget about the previous route diff --git a/contributing/code/standards.rst b/contributing/code/standards.rst index 134da5c1196..e8af77af491 100644 --- a/contributing/code/standards.rst +++ b/contributing/code/standards.rst @@ -47,32 +47,29 @@ short example containing most features described below:: */ class FooBar { - const SOME_CONST = 42; + public const SOME_CONST = 42; /** * @var string */ private $fooBar; - private $qux; /** - * @param string $dummy Some argument description + * @param $dummy some argument description */ - public function __construct($dummy, Qux $qux) + public function __construct(string $dummy, Qux $qux) { $this->fooBar = $this->transformText($dummy); $this->qux = $qux; } /** - * @return string - * * @deprecated */ - public function someDeprecatedMethod() + public function someDeprecatedMethod(): string { - @trigger_error(sprintf('The %s() method is deprecated since vendor-name/package-name 2.8 and will be removed in 3.0. Use Acme\Baz::someMethod() instead.', __METHOD__), E_USER_DEPRECATED); + trigger_deprecation('symfony/package-name', '5.1', 'The %s() method is deprecated, use Acme\Baz::someMethod() instead.', __METHOD__); return Baz::someMethod(); } @@ -80,14 +77,11 @@ short example containing most features described below:: /** * Transforms the input given as the first argument. * - * @param bool|string $dummy Some argument description - * @param array $options An options collection to be used within the transformation - * - * @return string|null The transformed input + * @param $options an options collection to be used within the transformation * - * @throws \RuntimeException When an invalid option is provided + * @throws \RuntimeException when an invalid option is provided */ - private function transformText($dummy, array $options = []) + private function transformText(bool|string $dummy, array $options = []): ?string { $defaultOptions = [ 'some_default' => 'values', @@ -100,16 +94,13 @@ short example containing most features described below:: } } - $mergedOptions = array_merge( - $defaultOptions, - $options - ); + $mergedOptions = array_merge($defaultOptions, $options); if (true === $dummy) { return 'something'; } - if (is_string($dummy)) { + if (\is_string($dummy)) { if ('values' === $mergedOptions['some_default']) { return substr($dummy, 0, 5); } @@ -122,11 +113,8 @@ short example containing most features described below:: /** * Performs some basic operations for a given value. - * - * @param mixed $value Some value to operate against - * @param bool $theSwitch Some switch to control the method's flow */ - private function performOperations($value = null, $theSwitch = false) + private function performOperations(mixed $value = null, bool $theSwitch = false) { if (!$theSwitch) { return; @@ -162,6 +150,8 @@ Structure * Use ``return null;`` when a function explicitly returns ``null`` values and use ``return;`` when the function returns ``void`` values; +* Do not add the ``void`` return type to methods in tests; + * Use braces to indicate control structure body regardless of the number of statements it contains; @@ -180,17 +170,15 @@ Structure to increase readability; * Declare all the arguments on the same line as the method/function name, no - matter how many arguments there are; + matter how many arguments there are. The only exception are constructor methods + using `constructor property promotion`_, where each parameter must be on a new + line with `trailing comma`_; * Use parentheses when instantiating classes regardless of the number of arguments the constructor has; * Exception and error message strings must be concatenated using :phpfunction:`sprintf`; -* Calls to :phpfunction:`trigger_error` with type ``E_USER_DEPRECATED`` must be - switched to opt-in via ``@`` operator. - Read more at :ref:`contributing-code-conventions-deprecations`; - * Do not use ``else``, ``elseif``, ``break`` after ``if`` and ``case`` conditions which return or throw something; @@ -267,19 +255,28 @@ Service Naming Conventions Documentation ~~~~~~~~~~~~~ -* Add PHPDoc blocks for all classes, methods, and functions (though you may - be asked to remove PHPDoc that do not add value); +* Add PHPDoc blocks for classes, methods, and functions only when they add + relevant information that does not duplicate the name, native type + declaration or context (e.g. ``instanceof`` checks); + +* Only use annotations and types defined in `the PHPDoc reference`_. In + order to improve types for static analysis, the following annotations are + also allowed: + + * `Generics`_, with the exception of ``@template-covariant``. + * `Conditional return types`_ using the vendor-prefixed ``@psalm-return``; + * `Class constants`_; + * `Callable types`_; * Group annotations together so that annotations of the same type immediately follow each other, and annotations of a different type are separated by a single blank line; -* Omit the ``@return`` tag if the method does not return anything; - -* The ``@package`` and ``@subpackage`` annotations are not used; +* Omit the ``@return`` annotation if the method does not return anything; -* Don't inline PHPDoc blocks, even when they contain just one tag (e.g. don't - put ``/** {@inheritdoc} */`` in a single line); +* Don't use one-line PHPDoc blocks on classes, methods and functions, even + when they contain just one annotation (e.g. don't put ``/** {@inheritdoc} */`` + in a single line); * When adding a new class or when making significant changes to an existing class, an ``@author`` tag with personal contact information may be added, or expanded. @@ -303,3 +300,10 @@ License .. _`camelCase`: https://en.wikipedia.org/wiki/Camel_case .. _`UpperCamelCase`: https://en.wikipedia.org/wiki/Camel_case .. _`snake_case`: https://en.wikipedia.org/wiki/Snake_case +.. _`constructor property promotion`: https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion +.. _`trailing comma`: https://wiki.php.net/rfc/trailing_comma_in_parameter_list +.. _`the PHPDoc reference`: https://docs.phpdoc.org/3.0/guide/references/phpdoc/index.html +.. _`Conditional return types`: https://psalm.dev/docs/annotating_code/type_syntax/conditional_types/ +.. _`Class constants`: https://psalm.dev/docs/annotating_code/type_syntax/value_types/#regular-class-constants +.. _`Callable types`: https://psalm.dev/docs/annotating_code/type_syntax/callable_types/ +.. _`Generics`: https://psalm.dev/docs/annotating_code/templated_annotations/ diff --git a/contributing/community/releases.rst b/contributing/community/releases.rst index 916a44f3894..774eca7a24d 100644 --- a/contributing/community/releases.rst +++ b/contributing/community/releases.rst @@ -7,20 +7,19 @@ release and maintain its different versions. Symfony releases follow the `semantic versioning`_ strategy and they are published through a *time-based model*: -* A new **Symfony patch version** (e.g. 4.4.12, 5.1.9) comes out roughly every +* A new **Symfony patch version** (e.g. 4.4.43, 5.4.10, 6.1.2) comes out roughly every month. It only contains bug fixes, so you can safely upgrade your applications; -* A new **Symfony minor version** (e.g. 4.4, 5.0, 5.1) comes out every *six months*: +* A new **Symfony minor version** (e.g. 4.4, 5.4, 6.1) comes out every *six months*: one in *May* and one in *November*. It contains bug fixes and new features, can contain new deprecations but it doesn't include any breaking change, so you can safely upgrade your applications; -* A new **Symfony major version** (e.g. 4.0, 5.0) comes out every *two years* - in November of odd years (e.g. 2019, 2021). - It can contain breaking changes, so you may need to do some changes in your - applications before upgrading. +* A new **Symfony major version** (e.g. 5.0, 6.0, 7.0) comes out every *two years* + in November of odd years (e.g. 2019, 2021, 2023). It can contain breaking changes, + so you may need to do some changes in your applications before upgrading. .. tip:: - `Subscribe to Symfony Roadmap notifications`_ to receive an email when a new + `Subscribe to Symfony Release notifications`_ to receive an email when a new Symfony version is published or when a Symfony version reaches its end of life. .. _contributing-release-development: @@ -28,6 +27,13 @@ published through a *time-based model*: Development ----------- +.. note:: + + The Symfony project is an open-source community-driven development framework. + There is no roadmap written or defined in advance. Every feature request + may or may not be developed in future versions based on the community. + Symfony Core Team members can help move things forward if there's enough interest. + The full development period for any major or minor version lasts six months and is divided into two phases: @@ -44,7 +50,7 @@ final release. .. tip:: - Check out the `Symfony Roadmap`_ to learn more about any specific version. + Check out the `Symfony Release`_ to learn more about any specific version. .. _contributing-release-maintenance: .. _symfony-versions: @@ -94,12 +100,12 @@ two versions: the new major one (e.g. 5.0) and the latest version of the previous branch (e.g. 4.4). Both versions have the same new features, but they differ in the deprecated -features. The oldest version (4.4 in this example) contains all the deprecated -features whereas the new version (5.0 in this example) removes all of them. +features. The oldest version (5.4 in this example) contains all the deprecated +features whereas the new version (6.0 in this example) removes all of them. -This allows you to upgrade your projects to the latest minor version (e.g. 4.4), +This allows you to upgrade your projects to the latest minor version (e.g. 5.4), see all the deprecation messages and fix them. Once you have fixed all those -deprecations, you can upgrade to the new major version (e.g. 5.0) without +deprecations, you can upgrade to the new major version (e.g. 6.0) without effort, because it contains the same features (the only difference are the deprecated features, which your project no longer uses). @@ -156,6 +162,6 @@ period to upgrade. Companies wanting more stability use the LTS versions: a new version is published every two years and there is a year to upgrade. .. _`semantic versioning`: https://semver.org/ -.. _`Subscribe to Symfony Roadmap notifications`: https://symfony.com/account/notifications -.. _`Symfony Roadmap`: https://symfony.com/releases +.. _`Subscribe to Symfony Release notifications`: https://symfony.com/account/notifications +.. _`Symfony Release`: https://symfony.com/releases .. _`professional Symfony support`: https://sensiolabs.com/ diff --git a/contributing/community/reviews.rst b/contributing/community/reviews.rst index bca36099eeb..ba08e4ffd36 100644 --- a/contributing/community/reviews.rst +++ b/contributing/community/reviews.rst @@ -150,7 +150,7 @@ Pick a pull request from the `PRs in need of review`_ and follow these steps: * Does the code break backward compatibility? If yes, does the PR header say so? * Does the PR contain deprecations? If yes, does the PR header say so? Does - the code contain ``trigger_error()`` statements for all deprecated + the code contain ``trigger_deprecation()`` statements for all deprecated features? * Are all deprecations and backward compatibility breaks documented in the latest UPGRADE-X.X.md file? Do those explanations contain "Before"/"After" diff --git a/contributing/documentation/format.rst b/contributing/documentation/format.rst index 297db06adf2..29b3cf6f8be 100644 --- a/contributing/documentation/format.rst +++ b/contributing/documentation/format.rst @@ -104,6 +104,7 @@ Markup Format Use It to Display ``html+php`` PHP code blended with HTML ``ini`` INI ``php-annotations`` PHP Annotations +``php-attributes`` PHP Attributes =================== ====================================== Adding Links @@ -173,39 +174,39 @@ If you are documenting a brand new feature, a change or a deprecation that's been made in Symfony, you should precede your description of the change with the corresponding directive and a short description: -For a new feature or a behavior change use the ``.. versionadded:: 4.x`` +For a new feature or a behavior change use the ``.. versionadded:: 6.x`` directive: .. code-block:: rst - .. versionadded:: 4.2 + .. versionadded:: 6.2 - Named autowiring aliases have been introduced in Symfony 4.2. + ... ... ... was introduced in Symfony 6.2. If you are documenting a behavior change, it may be helpful to *briefly* describe how the behavior has changed: .. code-block:: rst - .. versionadded:: 4.2 + .. versionadded:: 6.2 - Support for ICU MessageFormat was introduced in Symfony 4.2. Prior to this, - pluralization was managed by the ``transChoice`` method. + ... ... ... was introduced in Symfony 6.2. Prior to this, + ... ... ... ... ... ... ... ... . -For a deprecation use the ``.. deprecated:: 4.x`` directive: +For a deprecation use the ``.. deprecated:: 6.x`` directive: .. code-block:: rst - .. deprecated:: 4.2 + .. deprecated:: 6.2 - Not passing the root node name to ``TreeBuilder`` was deprecated in Symfony 4.2. + ... ... ... was deprecated in Symfony 6.2. -Whenever a new major version of Symfony is released (e.g. 5.0, 6.0, etc), -a new branch of the documentation is created from the x.4 branch of the previous -major version. At this point, all the ``versionadded`` and ``deprecated`` tags for -Symfony versions that have a lower major version will be removed. For example, if -Symfony 5.0 were released today, 4.0 to 4.4 ``versionadded`` and ``deprecated`` -tags would be removed from the new ``5.0`` branch. +Whenever a new major version of Symfony is released (e.g. 6.0, 7.0, etc), a new +branch of the documentation is created from the ``x.4`` branch of the previous +major version. At this point, all the ``versionadded`` and ``deprecated`` tags +for Symfony versions that have a lower major version will be removed. For +example, if Symfony 6.0 were released today, 5.0 to 5.4 ``versionadded`` and +``deprecated`` tags would be removed from the new ``6.0`` branch. .. _reStructuredText: https://docutils.sourceforge.io/rst.html .. _Sphinx: https://www.sphinx-doc.org/ diff --git a/contributing/documentation/standards.rst b/contributing/documentation/standards.rst index dc43258052e..7372d7058b1 100644 --- a/contributing/documentation/standards.rst +++ b/contributing/documentation/standards.rst @@ -88,9 +88,9 @@ Configuration examples should show all supported formats using (and their orders) are: * **Configuration** (including services): YAML, XML, PHP -* **Routing**: Annotations, YAML, XML, PHP -* **Validation**: Annotations, YAML, XML, PHP -* **Doctrine Mapping**: Annotations, YAML, XML, PHP +* **Routing**: Attributes, YAML, XML, PHP +* **Validation**: Attributes, YAML, XML, PHP +* **Doctrine Mapping**: Attributes, YAML, XML, PHP * **Translation**: XML, YAML, PHP Example @@ -191,7 +191,7 @@ In addition, documentation follows these rules: * trivial .. _`the Sphinx documentation`: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks -.. _`Twig Coding Standards`: https://twig.symfony.com/doc/2.x/coding_standards.html +.. _`Twig Coding Standards`: https://twig.symfony.com/doc/3.x/coding_standards.html .. _`reserved by the IANA`: https://tools.ietf.org/html/rfc2606#section-3 .. _`American English`: https://en.wikipedia.org/wiki/American_English .. _`American English Oxford Dictionary`: https://www.lexico.com/definition/american_english diff --git a/controller.rst b/controller.rst index e04bfd7a43d..626ac34e143 100644 --- a/controller.rst +++ b/controller.rst @@ -33,9 +33,7 @@ class:: class LuckyController { - /** - * @Route("/lucky/number/{max}", name="app_lucky_number") - */ + #[Route('/lucky/number/{max}', name: 'app_lucky_number')] public function number(int $max): Response { $number = random_int(0, $max); @@ -61,10 +59,10 @@ This controller is pretty straightforward: * *line 7*: The class can technically be called anything, but it's suffixed with ``Controller`` by convention. -* *line 12*: The action method is allowed to have a ``$max`` argument thanks to the +* *line 10*: The action method is allowed to have a ``$max`` argument thanks to the ``{max}`` :doc:`wildcard in the route `. -* *line 16*: The controller creates and returns a ``Response`` object. +* *line 14*: The controller creates and returns a ``Response`` object. .. index:: single: Controller; Routes and controllers @@ -73,8 +71,8 @@ Mapping a URL to a Controller ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In order to *view* the result of this controller, you need to map a URL to it via -a route. This was done above with the ``@Route("/lucky/number/{max}")`` -:ref:`route annotation `. +a route. This was done above with the ``#[Route('/lucky/number/{max}')]`` +:ref:`route attribute `. To see your page, go to this URL in your browser: http://localhost:8000/lucky/number/100 @@ -153,7 +151,7 @@ and ``redirect()`` methods:: // redirects to a route and maintains the original query string parameters return $this->redirectToRoute('blog_show', $request->query->all()); - + // redirects to the current route (e.g. for Post/Redirect/Get pattern): return $this->redirectToRoute($request->attributes->get('_route')); @@ -205,9 +203,7 @@ If you need a service in a controller, type-hint an argument with its class use Symfony\Component\HttpFoundation\Response; // ... - /** - * @Route("/lucky/number/{max}") - */ + #[Route('/lucky/number/{max}')] public function number(int $max, LoggerInterface $logger): Response { $logger->info('We are logging!'); @@ -223,66 +219,44 @@ command: $ php bin/console debug:autowiring -If you need control over the *exact* value of an argument, you can :ref:`bind ` -the argument by its name: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - # explicitly configure the service - App\Controller\LuckyController: - tags: [controller.service_arguments] - bind: - # for any $logger argument, pass this specific service - $logger: '@monolog.logger.doctrine' - # for any $projectDir argument, pass this parameter value - $projectDir: '%kernel.project_dir%' - - .. code-block:: xml - - - - - - - - - - - - - %kernel.project_dir% - - - - - .. code-block:: php - - // config/services.php - use App\Controller\LuckyController; - use Symfony\Component\DependencyInjection\Reference; - - $container->register(LuckyController::class) - ->addTag('controller.service_arguments') - ->setBindings([ - '$logger' => new Reference('monolog.logger.doctrine'), - '$projectDir' => '%kernel.project_dir%', - ]) - ; - -Like with all services, you can also use regular :ref:`constructor injection ` -in your controllers. +.. tip:: + + If you need control over the *exact* value of an argument, or require a parameter, + you can use the ``#[Autowire]`` attribute:: + + // ... + use Psr\Log\LoggerInterface; + use Symfony\Component\DependencyInjection\Attribute\Autowire; + use Symfony\Component\HttpFoundation\Response; + + class LuckyController extends AbstractController + { + public function number( + int $max, + + // inject a specific logger service + #[Autowire(service: 'monolog.logger.request')] + LoggerInterface $logger, + + // or inject parameter values + #[Autowire('%kernel.project_dir%')] + string $projectDir + ): Response + { + $logger->info('We are logging!'); + // ... + } + } + + You can read more about this attribute in :ref:`autowire-attribute`. + + .. versionadded:: 6.1 + + The ``#[Autowire]`` attribute was introduced in Symfony 6.1. + +Like with all services, you can also use regular +:ref:`constructor injection ` in your +controllers. For more information about services, see the :doc:`/service_container` article. @@ -315,10 +289,6 @@ use: created: templates/product/new.html.twig created: templates/product/show.html.twig -.. versionadded:: 1.2 - - The ``make:crud`` command was introduced in MakerBundle 1.2. - .. index:: single: Controller; Managing errors single: Controller; 404 pages @@ -400,7 +370,7 @@ Request object. Managing the Session -------------------- -Symfony provides a session service that you can use to store information +Symfony provides a session object that you can use to store information about the user between requests. Session is enabled by default, but will only be started if you read or write from it. diff --git a/controller/argument_value_resolver.rst b/controller/argument_value_resolver.rst deleted file mode 100644 index aaf1fa6d390..00000000000 --- a/controller/argument_value_resolver.rst +++ /dev/null @@ -1,263 +0,0 @@ -.. index:: - single: Controller; Argument Value Resolvers - -Extending Action Argument Resolving -=================================== - -In the :doc:`controller guide `, you've learned that you can get the -:class:`Symfony\\Component\\HttpFoundation\\Request` object via an argument in -your controller. This argument has to be type-hinted by the ``Request`` class -in order to be recognized. This is done via the -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver`. By -creating and registering custom argument value resolvers, you can extend this -functionality. - -.. _functionality-shipped-with-the-httpkernel: - -Built-In Value Resolvers ------------------------- - -Symfony ships with the following value resolvers in the -:doc:`HttpKernel component `: - -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\RequestAttributeValueResolver` - Attempts to find a request attribute that matches the name of the argument. - -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\RequestValueResolver` - Injects the current ``Request`` if type-hinted with ``Request`` or a class - extending ``Request``. - -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\ServiceValueResolver` - Injects a service if type-hinted with a valid service class or interface. This - works like :doc:`autowiring `. - -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\SessionValueResolver` - Injects the configured session class implementing ``SessionInterface`` if - type-hinted with ``SessionInterface`` or a class implementing - ``SessionInterface``. - -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\DefaultValueResolver` - Will set the default value of the argument if present and the argument - is optional. - -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\VariadicValueResolver` - Verifies if the request data is an array and will add all of them to the - argument list. When the action is called, the last (variadic) argument will - contain all the values of this array. - -In addition, some components and official bundles provide other value resolvers: - -:class:`Symfony\\Component\\Security\\Http\\Controller\\UserValueResolver` - Injects the object that represents the current logged in user if type-hinted - with ``UserInterface``. Default value can be set to ``null`` in case - the controller can be accessed by anonymous users. It requires installing - the :doc:`Security component `. - -:class:`Symfony\\Bundle\\SecurityBundle\\SecurityUserValueResolver` - Injects the object that represents the current logged in user if type-hinted - with ``UserInterface``. Default value can be set to ``null`` in case - the controller can be accessed by anonymous users. It requires installing - the `SecurityBundle`_. - -.. deprecated:: 4.1 - - The ``SecurityUserValueResolver`` was deprecated in Symfony 4.1 in favor of - :class:`Symfony\\Component\\Security\\Http\\Controller\\UserValueResolver`. - -Adding a Custom Value Resolver ------------------------------- - -In the next example, you'll create a value resolver to inject the object that -represents the current user whenever a controller method type-hints an argument -with the ``User`` class:: - - // src/Controller/UserController.php - namespace App\Controller; - - use App\Entity\User; - use Symfony\Component\HttpFoundation\Response; - - class UserController - { - public function index(User $user) - { - return new Response('Hello '.$user->getUsername().'!'); - } - } - -Beware that this feature is already provided by the `@ParamConverter`_ -annotation from the SensioFrameworkExtraBundle. If you have that bundle -installed in your project, add this config to disable the auto-conversion of -type-hinted method arguments: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/sensio_framework_extra.yaml - sensio_framework_extra: - request: - converters: true - auto_convert: false - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // config/packages/sensio_framework_extra.php - $container->loadFromExtension('sensio_framework_extra', [ - 'request' => [ - 'converters' => true, - 'auto_convert' => false, - ], - ]); - -Adding a new value resolver requires creating a class that implements -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentValueResolverInterface` -and defining a service for it. The interface defines two methods: - -``supports()`` - This method is used to check whether the value resolver supports the - given argument. ``resolve()`` will only be called when this returns ``true``. -``resolve()`` - This method will resolve the actual value for the argument. Once the value - is resolved, you must `yield`_ the value to the ``ArgumentResolver``. - -Both methods get the ``Request`` object, which is the current request, and an -:class:`Symfony\\Component\\HttpKernel\\ControllerMetadata\\ArgumentMetadata` -instance. This object contains all information retrieved from the method signature -for the current argument. - -Now that you know what to do, you can implement this interface. To get the -current ``User``, you need the current security token. This token can be -retrieved from the token storage:: - - // src/ArgumentResolver/UserValueResolver.php - namespace App\ArgumentResolver; - - use App\Entity\User; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; - use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; - use Symfony\Component\Security\Core\Security; - - class UserValueResolver implements ArgumentValueResolverInterface - { - private $security; - - public function __construct(Security $security) - { - $this->security = $security; - } - - public function supports(Request $request, ArgumentMetadata $argument): bool - { - if (User::class !== $argument->getType()) { - return false; - } - - return $this->security->getUser() instanceof User; - } - - public function resolve(Request $request, ArgumentMetadata $argument): iterable - { - yield $this->security->getUser(); - } - } - -In order to get the actual ``User`` object in your argument, the given value -must fulfill the following requirements: - -* An argument must be type-hinted as ``User`` in your action method signature; -* The value must be an instance of the ``User`` class. - -When all those requirements are met and ``true`` is returned, the -``ArgumentResolver`` calls ``resolve()`` with the same values as it called -``supports()``. - -That's it! Now all you have to do is add the configuration for the service -container. This can be done by tagging the service with ``controller.argument_value_resolver`` -and adding a priority. - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - _defaults: - # ... be sure autowiring is enabled - autowire: true - # ... - - App\ArgumentResolver\UserValueResolver: - tags: - - { name: controller.argument_value_resolver, priority: 50 } - - .. code-block:: xml - - - - - - - - - - - - - - - - - - .. code-block:: php - - // config/services.php - use App\ArgumentResolver\UserValueResolver; - - $container->autowire(UserValueResolver::class) - ->addTag('controller.argument_value_resolver', ['priority' => 50]) - ; - -While adding a priority is optional, it's recommended to add one to make sure -the expected value is injected. The built-in ``RequestAttributeValueResolver``, -which fetches attributes from the ``Request``, has a priority of ``100``. If your -resolver also fetches ``Request`` attributes, set a priority of ``100`` or more. -Otherwise, set a priority lower than ``100`` to make sure the argument resolver -is not triggered when the ``Request`` attribute is present (for example, when -passing the user along sub-requests). - -.. tip:: - - As you can see in the ``UserValueResolver::supports()`` method, the user - may not be available (e.g. when the controller is not behind a firewall). - In these cases, the resolver will not be executed. If no argument value - is resolved, an exception will be thrown. - - To prevent this, you can add a default value in the controller (e.g. ``User - $user = null``). The ``DefaultValueResolver`` is executed as the last - resolver and will use the default value if no value was already resolved. - -.. _`@ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html -.. _`yield`: https://www.php.net/manual/en/language.generators.syntax.php -.. _`SecurityBundle`: https://github.com/symfony/security-bundle diff --git a/controller/error_pages.rst b/controller/error_pages.rst index 3fe650054b8..070c228685f 100644 --- a/controller/error_pages.rst +++ b/controller/error_pages.rst @@ -155,32 +155,37 @@ automatically when installing ``symfony/framework-bundle``): .. code-block:: yaml - # config/routes/dev/framework.yaml - _errors: - resource: '@FrameworkBundle/Resources/config/routing/errors.xml' - prefix: /_error + # config/routes/framework.yaml + when@dev: + _errors: + resource: '@FrameworkBundle/Resources/config/routing/errors.xml' + prefix: /_error .. code-block:: xml - + - + + + .. code-block:: php - // config/routes/dev/framework.php + // config/routes/framework.php use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; return function (RoutingConfigurator $routes) { - $routes->import('@FrameworkBundle/Resources/config/routing/errors.xml') - ->prefix('/_error') - ; + if ('dev' === $routes->env()) { + $routes->import('@FrameworkBundle/Resources/config/routing/errors.xml') + ->prefix('/_error') + ; + } }; With this route added, you can use URLs like these to preview the *error* page @@ -215,7 +220,7 @@ contents, create a new Normalizer that supports the ``FlattenException`` input:: class MyCustomProblemNormalizer implements NormalizerInterface { - public function normalize($exception, string $format = null, array $context = []) + public function normalize($exception, string $format = null, array $context = []): array { return [ 'content' => 'This is my custom problem normalizer.', @@ -226,7 +231,7 @@ contents, create a new Normalizer that supports the ``FlattenException`` input:: ]; } - public function supportsNormalization($data, string $format = null) + public function supportsNormalization($data, string $format = null, array $context = []): bool { return $data instanceof FlattenException; } @@ -272,10 +277,12 @@ configuration option to point to it: .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'error_controller' => 'App\Controller\ErrorController::show', + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { // ... - ]); + $framework->errorController('App\Controller\ErrorController::show'); + }; The :class:`Symfony\\Component\\HttpKernel\\EventListener\\ErrorListener` class used by the FrameworkBundle as a listener of the ``kernel.exception`` event creates @@ -316,8 +323,8 @@ error pages. .. note:: - If your listener calls ``setResponse()`` on the - :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent`, + If your listener calls ``setThrowable()`` on the + :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` event, propagation will be stopped and the response will be sent to the client. diff --git a/controller/service.rst b/controller/service.rst index 5d9fe9ade26..5f259c08b07 100644 --- a/controller/service.rst +++ b/controller/service.rst @@ -4,13 +4,29 @@ How to Define Controllers as Services ===================================== -In Symfony, a controller does *not* need to be registered as a service. But if you're -using the :ref:`default services.yaml configuration `, -your controllers *are* already registered as services. This means you can use dependency -injection like any other normal service. +In Symfony, a controller does *not* need to be registered as a service. But if +you're using the :ref:`default services.yaml configuration `, +and your controllers extend the `AbstractController`_ class, they *are* automatically +registered as services. This means you can use dependency injection like any +other normal service. + +If your controllers don't extend the `AbstractController`_ class, you must +explicitly mark your controller services as ``public``. Alternatively, you can +apply the ``controller.service_arguments`` tag to your controller services. This +will make the tagged services ``public`` and will allow you to inject services +in method parameters: -Referencing your Service from Routing -------------------------------------- +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + + # 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'] Registering your controller as a service is the first step, but you also need to update your routing config to reference the service properly, so that Symfony @@ -23,7 +39,7 @@ a service like: ``App\Controller\HelloController::index``: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Controller/HelloController.php namespace App\Controller; @@ -32,9 +48,7 @@ a service like: ``App\Controller\HelloController::index``: class HelloController { - /** - * @Route("/hello", name="hello", methods={"GET"}) - */ + #[Route('/hello', name: 'hello', methods: ['GET'])] public function index() { // ... @@ -86,7 +100,7 @@ which is a common practice when following the `ADR pattern`_ .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Controller/Hello.php namespace App\Controller; @@ -94,9 +108,7 @@ which is a common practice when following the `ADR pattern`_ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; - /** - * @Route("/hello/{name}", name="hello") - */ + #[Route('/hello/{name}', name: 'hello')] class Hello { public function __invoke($name = 'World') @@ -182,12 +194,11 @@ Base Controller Methods and Their Service Replacements ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The best way to see how to replace base ``Controller`` convenience methods is to -look at the `ControllerTrait`_ that holds its logic. +look at the `AbstractController`_ class that holds its logic. If you want to know what type-hints to use for each service, see the ``getSubscribedServices()`` method in `AbstractController`_. -.. _`Controller class source code`: https://github.com/symfony/symfony/blob/4.4/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php -.. _`ControllerTrait`: https://github.com/symfony/symfony/blob/4.4/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php +.. _`Controller class source code`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php .. _`AbstractController`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php .. _`ADR pattern`: https://en.wikipedia.org/wiki/Action%E2%80%93domain%E2%80%93responder diff --git a/controller/soap_web_service.rst b/controller/soap_web_service.rst index effa613c1c5..43224116704 100644 --- a/controller/soap_web_service.rst +++ b/controller/soap_web_service.rst @@ -27,22 +27,27 @@ In this case, the SOAP service will allow the client to call a method called // src/Service/HelloService.php namespace App\Service; + use Symfony\Component\Mailer\MailerInterface; + use Symfony\Component\Mime\Email; + class HelloService { - private $mailer; + private MailerInterface $mailer; - public function __construct(\Swift_Mailer $mailer) + public function __construct(MailerInterface $mailer) { $this->mailer = $mailer; } - public function hello($name) + public function hello(string $name): string { - $message = (new \Swift_Message('Hello Service')) - ->setTo('me@example.com') - ->setBody($name.' says hi!'); + $email = (new Email()) + ->from('admin@example.com') + ->to('me@example.com') + ->subject('Hello Service') + ->text($name.' says hi!'); - $this->mailer->send($message); + $this->mailer->send($email); return 'Hello, '.$name; } @@ -66,9 +71,7 @@ can be retrieved via ``/soap?wsdl``:: class HelloServiceController extends AbstractController { - /** - * @Route("/soap") - */ + #[Route('/soap')] public function index(HelloService $helloService) { $soapServer = new \SoapServer('/path/to/hello.wsdl'); diff --git a/controller/upload_file.rst b/controller/upload_file.rst index dad80ee957d..158e7167a0b 100644 --- a/controller/upload_file.rst +++ b/controller/upload_file.rst @@ -24,9 +24,7 @@ add a PDF brochure for each product. To do so, add a new property called { // ... - /** - * @ORM\Column(type="string") - */ + #[ORM\Column(type: 'string')] private $brochureFilename; public function getBrochureFilename() @@ -129,13 +127,12 @@ Finally, you need to update the code of the controller that handles the form:: use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\String\Slugger\SluggerInterface; class ProductController extends AbstractController { - /** - * @Route("/product/new", name="app_product_new") - */ - public function new(Request $request) + #[Route('/product/new', name: 'app_product_new')] + public function new(Request $request, SluggerInterface $slugger) { $product = new Product(); $form = $this->createForm(ProductType::class, $product); @@ -150,7 +147,7 @@ Finally, you need to update the code of the controller that handles the form:: if ($brochureFile) { $originalFilename = pathinfo($brochureFile->getClientOriginalName(), PATHINFO_FILENAME); // this is needed to safely include the file name as part of the URL - $safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename); + $safeFilename = $slugger->slug($originalFilename); $newFilename = $safeFilename.'-'.uniqid().'.'.$brochureFile->guessExtension(); // Move the file to the directory where brochures are stored @@ -173,8 +170,8 @@ Finally, you need to update the code of the controller that handles the form:: return $this->redirectToRoute('app_product_list'); } - return $this->render('product/new.html.twig', [ - 'form' => $form->createView(), + return $this->renderForm('product/new.html.twig', [ + 'form' => $form, ]); } } @@ -198,20 +195,14 @@ There are some important things to consider in the code of the above controller: #. A well-known security best practice is to never trust the input provided by users. This also applies to the files uploaded by your visitors. The ``UploadedFile`` class provides methods to get the original file extension - (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getExtension`), - the original file size (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientSize`) + (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalExtension`), + the original file size (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getSize`) and the original file name (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalName`). However, they are considered *not safe* because a malicious user could tamper that information. That's why it's always better to generate a unique name and use the :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::guessExtension` method to let Symfony guess the right extension according to the file MIME type; -.. deprecated:: 4.1 - - The :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientSize` - method was deprecated in Symfony 4.1 and will be removed in Symfony 5.0. - Use ``getSize()`` instead. - You can use the following code to link to the PDF brochure of a product: .. code-block:: html+twig @@ -244,20 +235,23 @@ logic to a separate service:: use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\File\UploadedFile; + use Symfony\Component\String\Slugger\SluggerInterface; class FileUploader { private $targetDirectory; + private $slugger; - public function __construct($targetDirectory) + public function __construct($targetDirectory, SluggerInterface $slugger) { $this->targetDirectory = $targetDirectory; + $this->slugger = $slugger; } public function upload(UploadedFile $file) { $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME); - $safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename); + $safeFilename = $this->slugger->slug($originalFilename); $fileName = $safeFilename.'-'.uniqid().'.'.$file->guessExtension(); try { @@ -319,10 +313,17 @@ Then, define a service for this class: .. code-block:: php // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use App\Service\FileUploader; - $container->autowire(FileUploader::class) - ->setArgument('$targetDirectory', '%brochures_directory%'); + return static function (ContainerConfigurator $container) { + $services = $configurator->services(); + + $services->set(FileUploader::class) + ->arg('$targetDirectory', '%brochures_directory%') + ; + }; Now you're ready to use this service in the controller:: diff --git a/controller/value_resolver.rst b/controller/value_resolver.rst new file mode 100644 index 00000000000..2ece1fcaa7d --- /dev/null +++ b/controller/value_resolver.rst @@ -0,0 +1,320 @@ +.. index:: + single: Controller; Argument Value Resolvers + +Extending Action Argument Resolving +=================================== + +In the :doc:`controller guide `, you've learned that you can get the +:class:`Symfony\\Component\\HttpFoundation\\Request` object via an argument in +your controller. This argument has to be type-hinted by the ``Request`` class +in order to be recognized. This is done via the +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver`. By +creating and registering custom value resolvers, you can extend this +functionality. + +.. _functionality-shipped-with-the-httpkernel: + +Built-In Value Resolvers +------------------------ + +Symfony ships with the following value resolvers in the +:doc:`HttpKernel component `: + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\BackedEnumValueResolver` + Attempts to resolve a backed enum case from a route path parameter that matches the name of the argument. + Leads to a 404 Not Found response if the value isn't a valid backing value for the enum type. + + For example, if your backed enum is:: + + namespace App\Model; + + enum Suit: string + { + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'S'; + } + + And your controller contains the following:: + + class CardController + { + #[Route('/cards/{suit}')] + public function list(Suit $suit): Response + { + // ... + } + + // ... + } + + When requesting the ``/cards/H`` URL, the ``$suit`` variable will store the + ``Suit::Hearts`` case. + + Furthermore, you can limit route parameter's allowed values to + only one (or more) with ``EnumRequirement``:: + + use Symfony\Component\Routing\Requirement\EnumRequirement; + + // ... + + class CardController + { + #[Route('/cards/{suit}', requirements: [ + // this allows all values defined in the Enum + 'suit' => new EnumRequirement(Suit::class), + // this restricts the possible values to the Enum values listed here + 'suit' => new EnumRequirement([Suit::Diamonds, Suit::Spades]), + ])] + public function list(Suit $suit): Response + { + // ... + } + + // ... + } + + The example above allows requesting only ``/cards/D`` and ``/cards/S`` + URLs and leads to 404 Not Found response in two other cases. + + .. versionadded:: 6.1 + + The ``BackedEnumValueResolver`` and ``EnumRequirement`` were introduced in Symfony 6.1. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\RequestAttributeValueResolver` + Attempts to find a request attribute that matches the name of the argument. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\DateTimeValueResolver` + Attempts to find a request attribute that matches the name of the argument + and injects a ``DateTimeInterface`` object if type-hinted with a class + extending ``DateTimeInterface``. + + By default any input that can be parsed as a date string by PHP is accepted. + You can restrict how the input can be formatted with the + :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapDateTime` attribute. + + .. versionadded:: 6.1 + + The ``DateTimeValueResolver`` was introduced in Symfony 6.1. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\RequestValueResolver` + Injects the current ``Request`` if type-hinted with ``Request`` or a class + extending ``Request``. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\ServiceValueResolver` + Injects a service if type-hinted with a valid service class or interface. This + works like :doc:`autowiring `. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\SessionValueResolver` + Injects the configured session class implementing ``SessionInterface`` if + type-hinted with ``SessionInterface`` or a class implementing + ``SessionInterface``. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\DefaultValueResolver` + Will set the default value of the argument if present and the argument + is optional. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\UidValueResolver` + Attempts to convert any UID values from a route path parameter into UID objects. + Leads to a 404 Not Found response if the value isn't a valid UID. + + For example, the following will convert the token parameter into a ``UuidV4`` object:: + + // src/Controller/DefaultController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Uid\UuidV4; + + class DefaultController + { + #[Route('/share/{token}')] + public function share(UuidV4 $token): Response + { + // ... + } + } + + .. versionadded:: 6.1 + + The ``UidValueResolver`` was introduced in Symfony 6.1. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\VariadicValueResolver` + Verifies if the request data is an array and will add all of them to the + argument list. When the action is called, the last (variadic) argument will + contain all the values of this array. + +In addition, some components and official bundles provide other value resolvers: + +:class:`Symfony\\Component\\Security\\Http\\Controller\\UserValueResolver` + Injects the object that represents the current logged in user if type-hinted + with ``UserInterface``. You can also type-hint your own ``User`` class but you + must then add the ``#[CurrentUser]`` attribute to the argument. Default value + can be set to ``null`` in case the controller can be accessed by anonymous + users. It requires installing the :doc:`SecurityBundle `. + + If the argument is not nullable and there is no logged in user or the logged in + user has a user class not matching the type-hinted class, an ``AccessDeniedException`` + is thrown by the resolver to prevent access to the controller. + +PSR-7 Objects Resolver: + Injects a Symfony HttpFoundation ``Request`` object created from a PSR-7 object + of type :class:`Psr\\Http\\Message\\ServerRequestInterface`, + :class:`Psr\\Http\\Message\\RequestInterface` or :class:`Psr\\Http\\Message\\MessageInterface`. + It requires installing :doc:`the PSR-7 Bridge ` component. + +Adding a Custom Value Resolver +------------------------------ + +In the next example, you'll create a value resolver to inject an ID value +object whenever a controller argument has a type implementing +``IdentifierInterface`` (e.g. ``BookingId``):: + + // src/Controller/BookingController.php + namespace App\Controller; + + use App\Reservation\BookingId; + use Symfony\Component\HttpFoundation\Response; + + class BookingController + { + public function index(BookingId $id): Response + { + // ... do something with $id + } + } + +.. versionadded:: 6.2 + + The ``ValueResolverInterface`` was introduced in Symfony 6.2. Prior to + 6.2, you had to use the + :class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentValueResolverInterface`, + which defines different methods. + +Adding a new value resolver requires creating a class that implements +:class:`Symfony\\Component\\HttpKernel\\Controller\\ValueResolverInterface` +and defining a service for it. + +This interface contains a ``resolve()`` method, which is called for each +argument of the controller. It receives the current ``Request`` object and an +:class:`Symfony\\Component\\HttpKernel\\ControllerMetadata\\ArgumentMetadata` +instance, which contains all information from the method signature. + +The ``resolve()`` method should return either an empty array (if it cannot resolve +this argument) or an array with the resolved value(s). Usually arguments are +resolved as a single value, but variadic arguments require resolving multiple +values. That's why you must always return an array, even for single values: + +.. code-block:: php + + // src/ValueResolver/IdentifierValueResolver.php + namespace App\ValueResolver; + + use App\IdentifierInterface; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; + use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; + + class BookingIdValueResolver implements ValueResolverInterface + { + public function resolve(Request $request, ArgumentMetadata $argument): array + { + // get the argument type (e.g. BookingId) + $argumentType = $argument->getType(); + if ( + !$argumentType + || !is_subclass_of($argumentType, IdentifierInterface::class, true) + ) { + return []; + } + + // get the value from the request, based on the argument name + $value = $request->attributes->get($argument->getName()); + if (!is_string($value)) { + return []; + } + + // create and return the value object + return [$argumentType::fromString($value)]; + } + } + +This method first checks whether it can resolve the value: + +* The argument must be type-hinted with a class implementing a custom ``IdentifierInterface``; +* The argument name (e.g. ``$id``) must match the name of a request + attribute (e.g. using a ``/booking/{id}`` route placeholder). + +When those requirements are met, the method creates a new instance of the +custom value object and returns it as the value for this argument. + +That's it! Now all you have to do is add the configuration for the service +container. This can be done by tagging the service with ``controller.argument_value_resolver`` +and adding a priority: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + _defaults: + # ... be sure autowiring is enabled + autowire: true + # ... + + App\ArgumentResolver\UserValueResolver: + tags: + - { name: controller.argument_value_resolver, priority: 150 } + + .. code-block:: xml + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\ArgumentResolver\UserValueResolver; + + return static function (ContainerConfigurator $container) { + $services = $configurator->services(); + + $services->set(UserValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => 150]) + ; + }; + +While adding a priority is optional, it's recommended to add one to make sure +the expected value is injected. The built-in ``RequestAttributeValueResolver``, +which fetches attributes from the ``Request``, has a priority of ``100``. If your +resolver also fetches ``Request`` attributes, set a priority of ``100`` or more. +Otherwise, set a priority lower than ``100`` to make sure the argument resolver +is not triggered when the ``Request`` attribute is present. + +To ensure your resolvers are added in the right position you can run the following +command to see which argument resolvers are present and in which order they run: + +.. code-block:: terminal + + $ php bin/console debug:container debug.argument_resolver.inner --show-arguments diff --git a/create_framework/front_controller.rst b/create_framework/front_controller.rst index 733e764c94e..fded71a7b1c 100644 --- a/create_framework/front_controller.rst +++ b/create_framework/front_controller.rst @@ -38,7 +38,7 @@ Let's see it in action:: // framework/index.php require_once __DIR__.'/init.php'; - $name = $request->get('name', 'World'); + $name = $request->query->get('name', 'World'); $response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'))); $response->send(); @@ -98,7 +98,7 @@ Such a script might look like the following:: And here is for instance the new ``hello.php`` script:: // framework/hello.php - $name = $request->get('name', 'World'); + $name = $request->query->get('name', 'World'); $response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'))); In the ``front.php`` script, ``$map`` associates URL paths with their @@ -190,7 +190,7 @@ And the ``hello.php`` script can now be converted to a template: .. code-block:: html+php - get('name', 'World') ?> + query->get('name', 'World') ?> Hello diff --git a/create_framework/http_foundation.rst b/create_framework/http_foundation.rst index 14f4b00023b..2859c18553b 100644 --- a/create_framework/http_foundation.rst +++ b/create_framework/http_foundation.rst @@ -141,7 +141,7 @@ Now, let's rewrite our application by using the ``Request`` and the $request = Request::createFromGlobals(); - $name = $request->get('name', 'World'); + $name = $request->query->get('name', 'World'); $response = new Response(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'))); @@ -176,20 +176,20 @@ fingertips thanks to a nice and simple API:: // the URI being requested (e.g. /about) minus any query parameters $request->getPathInfo(); - // retrieve GET and POST variables respectively + // retrieves GET and POST variables respectively $request->query->get('foo'); $request->request->get('bar', 'default value if bar does not exist'); - // retrieve SERVER variables + // retrieves SERVER variables $request->server->get('HTTP_HOST'); // retrieves an instance of UploadedFile identified by foo $request->files->get('foo'); - // retrieve a COOKIE value + // retrieves a COOKIE value $request->cookies->get('PHPSESSID'); - // retrieve an HTTP request header, with normalized, lowercase keys + // retrieves a HTTP request header, with normalized, lowercase keys $request->headers->get('host'); $request->headers->get('content-type'); diff --git a/create_framework/http_kernel_httpkernelinterface.rst b/create_framework/http_kernel_httpkernelinterface.rst index ede06d965d4..f883b4a2e1d 100644 --- a/create_framework/http_kernel_httpkernelinterface.rst +++ b/create_framework/http_kernel_httpkernelinterface.rst @@ -16,7 +16,7 @@ goal by making our framework implement ``HttpKernelInterface``:: */ public function handle( Request $request, - $type = self::MASTER_REQUEST, + $type = self::MAIN_REQUEST, $catch = true ); } @@ -39,7 +39,7 @@ Update your framework so that it implements this interface:: public function handle( Request $request, - $type = HttpKernelInterface::MASTER_REQUEST, + $type = HttpKernelInterface::MAIN_REQUEST, $catch = true ) { // ... @@ -118,22 +118,28 @@ The Response class contains methods that let you configure the HTTP cache. One of the most powerful is ``setCache()`` as it abstracts the most frequently used caching strategies into a single array:: - $date = date_create_from_format('Y-m-d H:i:s', '2005-10-15 10:00:00'); - $response->setCache([ - 'public' => true, - 'etag' => 'abcde', - 'last_modified' => $date, - 'max_age' => 10, - 's_maxage' => 10, + 'must_revalidate' => false, + 'no_cache' => false, + 'no_store' => false, + 'no_transform' => false, + 'public' => true, + 'private' => false, + 'proxy_revalidate' => false, + 'max_age' => 600, + 's_maxage' => 600, + 'immutable' => true, + 'last_modified' => new \DateTime(), + 'etag' => 'abcdef' ]); // it is equivalent to the following code $response->setPublic(); + $response->setMaxAge(600); + $response->setSharedMaxAge(600); + $response->setImmutable(); + $response->setLastModified(new \DateTime()); $response->setEtag('abcde'); - $response->setLastModified($date); - $response->setMaxAge(10); - $response->setSharedMaxAge(10); When using the validation model, the ``isNotModified()`` method allows you to cut on the response time by short-circuiting the response generation as early as diff --git a/create_framework/unit_testing.rst b/create_framework/unit_testing.rst index fa7a93b077f..c2d04115812 100644 --- a/create_framework/unit_testing.rst +++ b/create_framework/unit_testing.rst @@ -188,7 +188,7 @@ Response:: $response = $framework->handle(new Request()); $this->assertEquals(200, $response->getStatusCode()); - $this->assertContains('Yep, this is a leap year!', $response->getContent()); + $this->assertStringContainsString('Yep, this is a leap year!', $response->getContent()); } In this test, we simulate a route that matches and returns a simple diff --git a/deployment.rst b/deployment.rst index 495cddb5505..f1f54446ef2 100644 --- a/deployment.rst +++ b/deployment.rst @@ -65,7 +65,7 @@ Using Platforms as a Service Using a Platform as a Service (PaaS) can be a great way to deploy your Symfony app quickly. There are many PaaS, but we recommend `Platform.sh`_ as it -provides a dedicated Symfony integration and help fund the Symfony development. +provides a dedicated Symfony integration and helps fund the Symfony development. Using Build Scripts and other Tools ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -166,6 +166,20 @@ most natural in your hosting environment. $ composer dump-env prod --empty + If ``composer`` is not installed on your server, you can generate this optimized + file with a command provided by Symfony itself, which you must register in + your application before using it: + + .. code-block:: yaml + + # config/services.yaml + services: + Symfony\Component\Dotenv\Command\DotenvDumpCommand: ~ + + .. code-block:: terminal + + $ APP_ENV=prod APP_DEBUG=0 php bin/console dotenv:dump + C) Install/Update your Vendors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/deployment/proxies.rst b/deployment/proxies.rst index 285dd221b11..da0380be420 100644 --- a/deployment/proxies.rst +++ b/deployment/proxies.rst @@ -22,27 +22,71 @@ Solution: ``setTrustedProxies()`` --------------------------------- To fix this, you need to tell Symfony which reverse proxy IP addresses to trust -and what headers your reverse proxy uses to send information:: +and what headers your reverse proxy uses to send information: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + # the IP address (or range) of your proxy + trusted_proxies: '192.0.0.1,10.0.0.0/8' + # trust *all* "X-Forwarded-*" headers + trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix'] + # or, if your proxy instead uses the "Forwarded" header + trusted_headers: ['forwarded'] + + .. code-block:: xml + + + + + + + + 192.0.0.1,10.0.0.0/8 + + + x-forwarded-for + x-forwarded-host + x-forwarded-proto + x-forwarded-port + x-forwarded-prefix + + + forwarded + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework + // the IP address (or range) of your proxy + ->trustedProxies('192.0.0.1,10.0.0.0/8') + // trust *all* "X-Forwarded-*" headers (the ! prefix means to not trust those headers) + ->trustedHeaders(['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix']) + // or, if your proxy instead uses the "Forwarded" header + ->trustedHeaders(['forwarded']) + ; + }; - // public/index.php - - // ... - $request = Request::createFromGlobals(); - - // tell Symfony about your reverse proxy - Request::setTrustedProxies( - // the IP address (or range) of your proxy - ['192.0.0.1', '10.0.0.0/8'], - - // trust *all* "X-Forwarded-*" headers - Request::HEADER_X_FORWARDED_ALL - - // or, if your proxy instead uses the "Forwarded" header - // Request::HEADER_FORWARDED +.. caution:: - // or, if you're using AWS ELB - // Request::HEADER_X_FORWARDED_AWS_ELB - ); + Enabling the ``Request::HEADER_X_FORWARDED_HOST`` option exposes the + application to `HTTP Host header attacks`_. Make sure the proxy really + sends an ``x-forwarded-host`` header. The Request object has several ``Request::HEADER_*`` constants that control exactly *which* headers from your reverse proxy are trusted. The argument is a bit field, @@ -64,23 +108,16 @@ In this case, you'll need to - *very carefully* - trust *all* proxies. other than your load balancers. For AWS, this can be done with `security groups`_. #. Once you've guaranteed that traffic will only come from your trusted reverse - proxies, configure Symfony to *always* trust incoming request:: - - // public/index.php - - // ... - Request::setTrustedProxies( - // trust *all* requests (the 'REMOTE_ADDR' string is replaced at - // run time by $_SERVER['REMOTE_ADDR']) - ['127.0.0.1', 'REMOTE_ADDR'], + proxies, configure Symfony to *always* trust incoming request: - // if you're using ELB, otherwise use a constant from above - Request::HEADER_X_FORWARDED_AWS_ELB - ); + .. code-block:: yaml -.. versionadded:: 4.4 - - The support for the ``REMOTE_ADDR`` option was introduced in Symfony 4.4. + # config/packages/framework.yaml + framework: + # ... + # trust *all* requests (the 'REMOTE_ADDR' string is replaced at + # run time by $_SERVER['REMOTE_ADDR']) + trusted_proxies: '127.0.0.1,REMOTE_ADDR' That's it! It's critical that you prevent traffic from all non-trusted sources. If you allow outside traffic, they could "spoof" their true IP address and @@ -96,6 +133,12 @@ other information. # .env TRUSTED_PROXIES=127.0.0.1,REMOTE_ADDR + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + trusted_proxies: '%env(TRUSTED_PROXIES)%' If you are also using a reverse proxy on top of your load balancer (e.g. `CloudFront`_), calling ``$request->server->get('REMOTE_ADDR')`` won't be @@ -107,11 +150,13 @@ trusted proxies. Custom Headers When Using a Reverse Proxy ----------------------------------------- -Some reverse proxies (like `CloudFront`_ with ``CloudFront-Forwarded-Proto``) may force you to use a custom header. -For instance you have ``Custom-Forwarded-Proto`` instead of ``X-Forwarded-Proto``. +Some reverse proxies (like `CloudFront`_ with ``CloudFront-Forwarded-Proto``) +may force you to use a custom header. For instance you have +``Custom-Forwarded-Proto`` instead of ``X-Forwarded-Proto``. -In this case, you'll need to set the header ``X-Forwarded-Proto`` with the value of -``Custom-Forwarded-Proto`` early enough in your application, i.e. before handling the request:: +In this case, you'll need to set the header ``X-Forwarded-Proto`` with the value +of ``Custom-Forwarded-Proto`` early enough in your application, i.e. before +handling the request:: // public/index.php @@ -123,4 +168,5 @@ In this case, you'll need to set the header ``X-Forwarded-Proto`` with the value .. _`security groups`: https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-groups.html .. _`CloudFront`: https://en.wikipedia.org/wiki/Amazon_CloudFront .. _`CloudFront IP ranges`: https://ip-ranges.amazonaws.com/ip-ranges.json +.. _`HTTP Host header attacks`: https://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html .. _`nginx realip module`: http://nginx.org/en/docs/http/ngx_http_realip_module.html diff --git a/doctrine.rst b/doctrine.rst index 46770aa35b5..632641d1f11 100644 --- a/doctrine.rst +++ b/doctrine.rst @@ -127,11 +127,6 @@ need. The command will ask you some questions - answer them like done below: > (press enter again to finish) -.. versionadded:: 1.3 - - The interactive behavior of the ``make:entity`` command was introduced - in MakerBundle 1.3. - Whoa! You now have a new ``src/Entity/Product.php`` file:: // src/Entity/Product.php @@ -140,27 +135,19 @@ Whoa! You now have a new ``src/Entity/Product.php`` file:: use App\Repository\ProductRepository; use Doctrine\ORM\Mapping as ORM; - /** - * @ORM\Entity(repositoryClass=ProductRepository::class) - */ + #[ORM\Entity(repositoryClass: ProductRepository::class)] class Product { - /** - * @ORM\Id() - * @ORM\GeneratedValue() - * @ORM\Column(type="integer") - */ - private $id; + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private int $id; - /** - * @ORM\Column(type="string", length=255) - */ - private $name; + #[ORM\Column(length: 255)] + private string $name; - /** - * @ORM\Column(type="integer") - */ - private $price; + #[ORM\Column] + private int $price; public function getId(): ?int { @@ -170,6 +157,10 @@ Whoa! You now have a new ``src/Entity/Product.php`` file:: // ... getter and setter methods } +.. note:: + + Starting in v1.44.0 - MakerBundle only supports entities using PHP attributes. + .. note:: Confused why the price is an integer? Don't worry: this is just an example. @@ -194,8 +185,8 @@ Whoa! You now have a new ``src/Entity/Product.php`` file:: This class is called an "entity". And soon, you'll be able to save and query Product objects to a ``product`` table in your database. Each property in the ``Product`` -entity can be mapped to a column in that table. This is usually done with annotations: -the ``@ORM\...`` comments that you see above each property: +entity can be mapped to a column in that table. This is usually done with attributes: +the ``#[ORM\Column(...)]`` comments that you see above each property: .. image:: /_images/doctrine/mapping_single_entity.png :align: center @@ -214,8 +205,8 @@ If you want to use XML instead of annotations, add ``type: xml`` and Be careful not to use reserved SQL keywords as your table or column names (e.g. ``GROUP`` or ``USER``). See Doctrine's `Reserved SQL keywords documentation`_ for details on how to escape these. Or, change the table name with - ``@ORM\Table(name="groups")`` above the class or configure the column name with - the ``name="group_name"`` option. + ``#[ORM\Table(name: 'groups')]`` above the class or configure the column name with + the ``name: 'group_name'`` option. .. _doctrine-creating-the-database-tables-schema: @@ -233,9 +224,11 @@ already installed: If everything worked, you should see something like this: +.. code-block:: text + SUCCESS! - Next: Review the new migration "src/Migrations/Version20180207231217.php" + Next: Review the new migration "migrations/Version20211116204726.php" Then: Run the migration with php bin/console doctrine:migrations:migrate If you open this file, it contains the SQL needed to update your database! To run @@ -290,9 +283,7 @@ methods: { // ... - + /** - + * @ORM\Column(type="text") - + */ + + #[ORM\Column(type: 'text')] + private $description; // getDescription() & setDescription() were also added @@ -359,19 +350,16 @@ and save it:: // ... use App\Entity\Product; - use Doctrine\ORM\EntityManagerInterface; + use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Annotation\Route; class ProductController extends AbstractController { - /** - * @Route("/product", name="create_product") - */ - public function createProduct(): Response + #[Route('/product', name: 'create_product')] + public function createProduct(ManagerRegistry $doctrine): Response { - // you can fetch the EntityManager via $this->getDoctrine() - // or you can add an argument to the action: createProduct(EntityManagerInterface $entityManager) - $entityManager = $this->getDoctrine()->getManager(); + $entityManager = $doctrine->getManager(); $product = new Product(); $product->setName('Keyboard'); @@ -397,26 +385,30 @@ you can query the database directly: .. code-block:: terminal - $ php bin/console doctrine:query:sql 'SELECT * FROM product' + $ php bin/console dbal:run-sql 'SELECT * FROM product' # on Windows systems not using Powershell, run this command instead: - # php bin/console doctrine:query:sql "SELECT * FROM product" + # php bin/console dbal:run-sql "SELECT * FROM product" Take a look at the previous example in more detail: .. _doctrine-entity-manager: -* **line 18** The ``$this->getDoctrine()->getManager()`` method gets Doctrine's +* **line 13** The ``ManagerRegistry $doctrine`` argument tells Symfony to + :ref:`inject the Doctrine service ` into the + controller method. + +* **line 15** The ``$doctrine->getManager()`` method gets Doctrine's *entity manager* object, which is the most important object in Doctrine. It's responsible for saving objects to, and fetching objects from, the database. -* **lines 20-23** In this section, you instantiate and work with the ``$product`` +* **lines 17-20** In this section, you instantiate and work with the ``$product`` object like any other normal PHP object. -* **line 26** The ``persist($product)`` call tells Doctrine to "manage" the +* **line 23** The ``persist($product)`` call tells Doctrine to "manage" the ``$product`` object. This does **not** cause a query to be made to the database. -* **line 29** When the ``flush()`` method is called, Doctrine looks through +* **line 26** When the ``flush()`` method is called, Doctrine looks through all of the objects that it's managing to see if they need to be persisted to the database. In this example, the ``$product`` object's data doesn't exist in the database, so the entity manager executes an ``INSERT`` query, @@ -443,14 +435,13 @@ some basic validation tasks:: use App\Entity\Product; use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Validator\Validator\ValidatorInterface; // ... class ProductController extends AbstractController { - /** - * @Route("/product", name="create_product") - */ + #[Route('/product', name: 'create_product')] public function createProduct(ValidatorInterface $validator): Response { $product = new Product(); @@ -498,10 +489,6 @@ doesn't replace the validation configuration entirely. You still need to add some :doc:`validation constraints ` to ensure that data provided by the user is correct. -.. versionadded:: 4.3 - - The automatic validation has been added in Symfony 4.3. - Fetching Objects from the Database ---------------------------------- @@ -513,18 +500,15 @@ be able to go to ``/product/1`` to see your new product:: use App\Entity\Product; use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Annotation\Route; // ... class ProductController extends AbstractController { - /** - * @Route("/product/{id}", name="product_show") - */ - public function show(int $id): Response + #[Route('/product/{id}', name: 'product_show')] + public function show(ManagerRegistry $doctrine, int $id): Response { - $product = $this->getDoctrine() - ->getRepository(Product::class) - ->find($id); + $product = $doctrine->getRepository(Product::class)->find($id); if (!$product) { throw $this->createNotFoundException( @@ -549,13 +533,12 @@ and injected by the dependency injection container:: use App\Entity\Product; use App\Repository\ProductRepository; use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Annotation\Route; // ... class ProductController extends AbstractController { - /** - * @Route("/product/{id}", name="product_show") - */ + #[Route('/product/{id}', name: 'product_show')] public function show(int $id, ProductRepository $productRepository): Response { $product = $productRepository @@ -575,7 +558,7 @@ job is to help you fetch entities of a certain class. Once you have a repository object, you have many helper methods:: - $repository = $this->getDoctrine()->getRepository(Product::class); + $repository = $doctrine->getRepository(Product::class); // look for a single Product by its primary key (usually "id") $product = $repository->find($id); @@ -633,13 +616,12 @@ Now, simplify your controller:: use App\Entity\Product; use App\Repository\ProductRepository; use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Annotation\Route; // ... class ProductController extends AbstractController { - /** - * @Route("/product/{id}", name="product_show") - */ + #[Route('/product/{id}', name: 'product_show')] public function show(Product $product): Response { // use the Product! @@ -664,16 +646,15 @@ with any PHP model:: use App\Entity\Product; use App\Repository\ProductRepository; use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Annotation\Route; // ... class ProductController extends AbstractController { - /** - * @Route("/product/edit/{id}") - */ - public function update(int $id): Response + #[Route('/product/edit/{id}', name: 'product_edit')] + public function update(ManagerRegistry $doctrine, int $id): Response { - $entityManager = $this->getDoctrine()->getManager(); + $entityManager = $doctrine->getManager(); $product = $entityManager->getRepository(Product::class)->find($id); if (!$product) { @@ -722,8 +703,7 @@ You've already seen how the repository object allows you to run basic queries without any work:: // from inside a controller - $repository = $this->getDoctrine()->getRepository(Product::class); - + $repository = $doctrine->getRepository(Product::class); $product = $repository->find($id); But what if you need a more complex query? When you generated your entity with @@ -790,9 +770,7 @@ Now, you can call this method on the repository:: // from inside a controller $minPrice = 1000; - $products = $this->getDoctrine() - ->getRepository(Product::class) - ->findAllGreaterThanPrice($minPrice); + $products = $doctrine->getRepository(Product::class)->findAllGreaterThanPrice($minPrice); // ... diff --git a/doctrine/associations.rst b/doctrine/associations.rst index a3c138c008f..8ebdadf7864 100644 --- a/doctrine/associations.rst +++ b/doctrine/associations.rst @@ -68,19 +68,16 @@ This will generate your new entity class:: // ... + #[ORM\Entity(repositoryClass: CategoryRepository::class)] class Category { - /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] private $id; - /** - * @ORM\Column(type="string") - */ - private $name; + #[ORM\Column] + private string $name; // ... getters and setters } @@ -143,7 +140,7 @@ the ``Product`` entity (and getter & setter methods): .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Product.php namespace App\Entity; @@ -153,9 +150,7 @@ the ``Product`` entity (and getter & setter methods): { // ... - /** - * @ORM\ManyToOne(targetEntity="App\Entity\Category", inversedBy="products") - */ + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'products')] private $category; public function getCategory(): ?Category @@ -214,7 +209,7 @@ class that will hold these objects: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Category.php namespace App\Entity; @@ -227,9 +222,7 @@ class that will hold these objects: { // ... - /** - * @ORM\OneToMany(targetEntity="App\Entity\Product", mappedBy="category") - */ + #[ORM\OneToMany(targetEntity: Product::class, mappedBy: 'category')] private $products; public function __construct() @@ -320,14 +313,14 @@ Now you can see this new code in action! Imagine you're inside a controller:: // ... use App\Entity\Category; use App\Entity\Product; + use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Annotation\Route; class ProductController extends AbstractController { - /** - * @Route("/product", name="product") - */ - public function index(): Response + #[Route('/product', name: 'product')] + public function index(ManagerRegistry $doctrine): Response { $category = new Category(); $category->setName('Computer Peripherals'); @@ -340,7 +333,7 @@ Now you can see this new code in action! Imagine you're inside a controller:: // relates this product to the category $product->setCategory($category); - $entityManager = $this->getDoctrine()->getManager(); + $entityManager = $doctrine->getManager(); $entityManager->persist($category); $entityManager->persist($product); $entityManager->flush(); @@ -386,12 +379,9 @@ before. First, fetch a ``$product`` object and then access its related class ProductController extends AbstractController { - public function show(int $id): Response + public function show(ManagerRegistry $doctrine, int $id): Response { - $product = $this->getDoctrine() - ->getRepository(Product::class) - ->find($id); - + $product = $doctrine->getRepository(Product::class)->find($id); // ... $categoryName = $product->getCategory()->getName(); @@ -422,11 +412,9 @@ direction:: // ... class ProductController extends AbstractController { - public function showProducts(int $id): Response + public function showProducts(ManagerRegistry $doctrine, int $id): Response { - $category = $this->getDoctrine() - ->getRepository(Category::class) - ->find($id); + $category = $doctrine->getRepository(Category::class)->find($id); $products = $category->getProducts(); @@ -445,9 +433,7 @@ by adding JOINs. a "proxy" object in place of the true object. Look again at the above example:: - $product = $this->getDoctrine() - ->getRepository(Product::class) - ->find($id); + $product = $doctrine->getRepository(Product::class)->find($id); $category = $product->getCategory(); @@ -517,11 +503,9 @@ object and its related ``Category`` in one query:: // ... class ProductController extends AbstractController { - public function show(int $id): Response + public function show(ManagerRegistry $doctrine, int $id): Response { - $product = $this->getDoctrine() - ->getRepository(Product::class) - ->findOneByIdJoinedToCategory($id); + $product = $doctrine->getRepository(Product::class)->findOneByIdJoinedToCategory($id); $category = $product->getCategory(); @@ -542,7 +526,7 @@ To update a relationship in the database, you *must* set the relationship on the *owning* side. The owning side is always where the ``ManyToOne`` mapping is set (for a ``ManyToMany`` relation, you can choose which side is the owning side). -Does this means it's not possible to call ``$category->addProduct()`` or +Does this mean it's not possible to call ``$category->addProduct()`` or ``$category->removeProduct()`` to update the database? Actually, it *is* possible, thanks to some clever code that the ``make:entity`` command generated:: @@ -597,16 +581,19 @@ on that ``Product`` will be set to ``null`` in the database. But, instead of setting the ``category_id`` to null, what if you want the ``Product`` to be *deleted* if it becomes "orphaned" (i.e. without a ``Category``)? To choose -that behavior, use the `orphanRemoval`_ option inside ``Category``:: +that behavior, use the `orphanRemoval`_ option inside ``Category``: - // src/Entity/Category.php +.. configuration-block:: - // ... + .. code-block:: php-attributes + + // src/Entity/Category.php + + // ... + + #[ORM\OneToMany(targetEntity: Product::class, mappedBy: 'category', orphanRemoval: true)] + private $products; - /** - * @ORM\OneToMany(targetEntity="App\Entity\Product", mappedBy="category", orphanRemoval=true) - */ - private $products; Thanks to this, if the ``Product`` is removed from the ``Category``, it will be removed from the database entirely. @@ -621,8 +608,8 @@ Doctrine's `Association Mapping Documentation`_. .. note:: - If you're using annotations, you'll need to prepend all annotations with - ``@ORM\`` (e.g. ``@ORM\OneToMany``), which is not reflected in Doctrine's + If you're using attributes, you'll need to prepend all attributes with + ``#[ORM\]`` (e.g. ``#[ORM\OneToMany]``), which is not reflected in Doctrine's documentation. .. _`Association Mapping Documentation`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/association-mapping.html diff --git a/doctrine/custom_dql_functions.rst b/doctrine/custom_dql_functions.rst index a6a47d4a18e..1d6a03fa597 100644 --- a/doctrine/custom_dql_functions.rst +++ b/doctrine/custom_dql_functions.rst @@ -57,24 +57,19 @@ In Symfony, you can register your custom DQL functions as follows: use App\DQL\NumericFunction; use App\DQL\SecondStringFunction; use App\DQL\StringFunction; + use Symfony\Config\DoctrineConfig; - $container->loadFromExtension('doctrine', [ - 'orm' => [ - // ... - 'dql' => [ - 'string_functions' => [ - 'test_string' => StringFunction::class, - 'second_string' => SecondStringFunction::class, - ], - 'numeric_functions' => [ - 'test_numeric' => NumericFunction::class, - ], - 'datetime_functions' => [ - 'test_datetime' => DatetimeFunction::class, - ], - ], - ], - ]); + return static function (DoctrineConfig $doctrine) { + $defaultDql = $doctrine->orm() + ->entityManager('default') + // ... + ->dql(); + + $defaultDql->stringFunction('test_string', StringFunction::class); + $defaultDql->stringFunction('second_string', SecondStringFunction::class); + $defaultDql->numericFunction('test_numeric', NumericFunction::class); + $defaultDql->datetimeFunction('test_datetime', DatetimeFunction::class); + }; .. note:: @@ -129,21 +124,15 @@ In Symfony, you can register your custom DQL functions as follows: // config/packages/doctrine.php use App\DQL\DatetimeFunction; + use Symfony\Config\DoctrineConfig; - $container->loadFromExtension('doctrine', [ - 'orm' => [ + return static function (DoctrineConfig $doctrine) { + $doctrine->orm() // ... - 'entity_managers' => [ - 'example_manager' => [ - // place your functions here - 'dql' => [ - 'datetime_functions' => [ - 'test_datetime' => DatetimeFunction::class, - ], - ], - ], - ], - ], - ]); + ->entityManager('example_manager') + // place your functions here + ->dql() + ->datetimeFunction('test_datetime', DatetimeFunction::class); + }; .. _`DQL User Defined Functions`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/cookbook/dql-user-defined-functions.html diff --git a/doctrine/dbal.rst b/doctrine/dbal.rst index ab1947bd1bb..a9d04674163 100644 --- a/doctrine/dbal.rst +++ b/doctrine/dbal.rst @@ -47,7 +47,7 @@ object:: // src/Controller/UserController.php namespace App\Controller; - use Doctrine\DBAL\Driver\Connection; + use Doctrine\DBAL\Connection; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; @@ -105,15 +105,13 @@ mapping types, read Doctrine's `Custom Mapping Types`_ section of their document // config/packages/doctrine.php use App\Type\CustomFirst; use App\Type\CustomSecond; + use Symfony\Config\DoctrineConfig; - $container->loadFromExtension('doctrine', [ - 'dbal' => [ - 'types' => [ - 'custom_first' => CustomFirst::class, - 'custom_second' => CustomSecond::class, - ], - ], - ]); + return static function (DoctrineConfig $doctrine) { + $dbal = $doctrine->dbal(); + $dbal->type('custom_first')->class(CustomFirst::class); + $dbal->type('custom_second')->class(CustomSecond::class); + }; Registering custom Mapping Types in the SchemaTool -------------------------------------------------- @@ -156,13 +154,13 @@ mapping type: .. code-block:: php // config/packages/doctrine.php - $container->loadFromExtension('doctrine', [ - 'dbal' => [ - 'mapping_types' => [ - 'enum' => 'string', - ], - ], - ]); + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine) { + $dbalDefault = $doctrine->dbal() + ->connection('default'); + $dbalDefault->mappingType('enum', 'string'); + }; .. _`PDO`: https://www.php.net/pdo .. _`Doctrine`: https://www.doctrine-project.org/ diff --git a/doctrine/events.rst b/doctrine/events.rst index b2a76b6ac56..e29cd82c599 100644 --- a/doctrine/events.rst +++ b/doctrine/events.rst @@ -53,27 +53,23 @@ define a callback for the ``prePersist`` Doctrine event: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Product.php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; - // When using annotations, don't forget to add @ORM\HasLifecycleCallbacks() + // When using attributes, don't forget to add #[ORM\HasLifecycleCallbacks] // to the class of the entity where you define the callback - /** - * @ORM\Entity() - * @ORM\HasLifecycleCallbacks() - */ + #[ORM\Entity] + #[ORM\HasLifecycleCallbacks] class Product { // ... - /** - * @ORM\PrePersist - */ + #[ORM\PrePersist] public function setCreatedAtValue(): void { $this->createdAt = new \DateTimeImmutable(); @@ -166,7 +162,7 @@ with the ``doctrine.event_listener`` tag: # this is the only required option for the lifecycle listener tag event: 'postPersist' - # listeners can define their priority in case multiple listeners are associated + # listeners can define their priority in case multiple subscribers or listeners are associated # to the same event (default priority = 0; higher numbers = listener is run earlier) priority: 500 @@ -184,7 +180,7 @@ with the ``doctrine.event_listener`` tag: @@ -200,22 +196,28 @@ with the ``doctrine.event_listener`` tag: .. code-block:: php // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use App\EventListener\SearchIndexer; - // listeners are applied by default to all Doctrine connections - $container->autowire(SearchIndexer::class) - ->addTag('doctrine.event_listener', [ - // this is the only required option for the lifecycle listener tag - 'event' => 'postPersist', + return static function (ContainerConfigurator $container) { + $services = $configurator->services(); + + // listeners are applied by default to all Doctrine connections + $services->set(SearchIndexer::class) + ->tag('doctrine.event_listener', [ + // this is the only required option for the lifecycle listener tag + 'event' => 'postPersist', - // listeners can define their priority in case multiple listeners are associated - // to the same event (default priority = 0; higher numbers = listener is run earlier) - 'priority' => 500, + // listeners can define their priority in case multiple subscribers or listeners are associated + // to the same event (default priority = 0; higher numbers = listener is run earlier) + 'priority' => 500, - # you can also restrict listeners to a specific Doctrine connection - 'connection' => 'default', - ]) - ; + # you can also restrict listeners to a specific Doctrine connection + 'connection' => 'default', + ]) + ; + }; .. tip:: @@ -223,6 +225,11 @@ with the ``doctrine.event_listener`` tag: Doctrine event is actually fired; whereas Doctrine subscribers are always loaded (and instantiated) by Symfony, making them less performant. +.. tip:: + + The value of the ``connection`` option can also be a + :ref:`configuration parameter `. + Doctrine Entity Listeners ------------------------- @@ -316,33 +323,35 @@ with the ``doctrine.orm.entity_listener`` tag: .. code-block:: php // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use App\Entity\User; use App\EventListener\UserChangedNotifier; - $container->autowire(UserChangedNotifier::class) - ->addTag('doctrine.orm.entity_listener', [ - // These are the options required to define the entity listener: - 'event' => 'postUpdate', - 'entity' => User::class, + return static function (ContainerConfigurator $container) { + $services = $configurator->services(); - // These are other options that you may define if needed: + $services->set(UserChangedNotifier::class) + ->tag('doctrine.orm.entity_listener', [ + // These are the options required to define the entity listener: + 'event' => 'postUpdate', + 'entity' => User::class, - // set the 'lazy' option to TRUE to only instantiate listeners when they are used - // 'lazy' => true, + // These are other options that you may define if needed: - // set the 'entity_manager' option if the listener is not associated to the default manager - // 'entity_manager' => 'custom', + // set the 'lazy' option to TRUE to only instantiate listeners when they are used + // 'lazy' => true, - // by default, Symfony looks for a method called after the event (e.g. postUpdate()) - // if it doesn't exist, it tries to execute the '__invoke()' method, but you can - // configure a custom method name with the 'method' option - // 'method' => 'checkUserChanges', - ]) - ; + // set the 'entity_manager' option if the listener is not associated to the default manager + // 'entity_manager' => 'custom', -.. versionadded:: 4.4 - - Support for invokable listeners (using the ``__invoke()`` method) was introduced in Symfony 4.4. + // by default, Symfony looks for a method called after the event (e.g. postUpdate()) + // if it doesn't exist, it tries to execute the '__invoke()' method, but you can + // configure a custom method name with the 'method' option + // 'method' => 'checkUserChanges', + ]) + ; + }; Doctrine Lifecycle Subscribers ------------------------------ @@ -411,8 +420,8 @@ and DoctrineBundle 2.1 (released May 25, 2020) or newer, this example will alrea work! Otherwise, :ref:`create a service ` for this subscriber and :doc:`tag it ` with ``doctrine.event_subscriber``. -If you need to associate the subscriber with a specific Doctrine connection, you -must do that in the manual service configuration: +If you need to configure some option of the subscriber (e.g. its priority or +Doctrine connection to use) you must do that in the manual service configuration: .. configuration-block:: @@ -424,7 +433,14 @@ must do that in the manual service configuration: App\EventListener\DatabaseActivitySubscriber: tags: - - { name: 'doctrine.event_subscriber', connection: 'default' } + - name: 'doctrine.event_subscriber' + + # subscribers can define their priority in case multiple subscribers or listeners are associated + # to the same event (default priority = 0; higher numbers = listener is run earlier) + priority: 500 + + # you can also restrict listeners to a specific Doctrine connection + connection: 'default' .. code-block:: xml @@ -435,8 +451,13 @@ must do that in the manual service configuration: + - + @@ -444,11 +465,24 @@ must do that in the manual service configuration: .. code-block:: php // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use App\EventListener\DatabaseActivitySubscriber; - $container->autowire(DatabaseActivitySubscriber::class) - ->addTag('doctrine.event_subscriber', ['connection' => 'default']) - ; + return static function (ContainerConfigurator $container) { + $services = $configurator->services(); + + $services->set(DatabaseActivitySubscriber::class) + ->tag('doctrine.event_subscriber'[ + // subscribers can define their priority in case multiple subscribers or listeners are associated + // to the same event (default priority = 0; higher numbers = listener is run earlier) + 'priority' => 500, + + // you can also restrict listeners to a specific Doctrine connection + 'connection' => 'default', + ]) + ; + }; .. tip:: diff --git a/doctrine/multiple_entity_managers.rst b/doctrine/multiple_entity_managers.rst index c609ad1291b..856e796a2e9 100644 --- a/doctrine/multiple_entity_managers.rst +++ b/doctrine/multiple_entity_managers.rst @@ -128,57 +128,47 @@ The following configuration code shows how you can configure two entity managers .. code-block:: php // config/packages/doctrine.php - $container->loadFromExtension('doctrine', [ - 'dbal' => [ - 'default_connection' => 'default', - 'connections' => [ - // configure these for your database server - 'default' => [ - 'url' => '%env(resolve:DATABASE_URL)%', - 'driver' => 'pdo_mysql', - 'server_version' => '5.7', - 'charset' => 'utf8mb4', - ], - // configure these for your database server - 'customer' => [ - 'url' => '%env(resolve:DATABASE_CUSTOMER_URL)%', - 'driver' => 'pdo_mysql', - 'server_version' => '5.7', - 'charset' => 'utf8mb4', - ], - ], - ], - - 'orm' => [ - 'default_entity_manager' => 'default', - 'entity_managers' => [ - 'default' => [ - 'connection' => 'default', - 'mappings' => [ - 'Main' => [ - 'is_bundle' => false, - 'type' => 'annotation', - 'dir' => '%kernel.project_dir%/src/Entity/Main', - 'prefix' => 'App\Entity\Main', - 'alias' => 'Main', - ], - ], - ], - 'customer' => [ - 'connection' => 'customer', - 'mappings' => [ - 'Customer' => [ - 'is_bundle' => false, - 'type' => 'annotation', - 'dir' => '%kernel.project_dir%/src/Entity/Customer', - 'prefix' => 'App\Entity\Customer', - 'alias' => 'Customer', - ], - ], - ], - ], - ], - ]); + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine) { + $doctrine->dbal()->defaultConnection('default'); + + // configure these for your database server + $doctrine->dbal() + ->connection('default') + ->url(env('DATABASE_URL')->resolve()) + ->driver('pdo_mysql') + ->serverVersion('5.7') + ->charset('utf8mb4'); + + // configure these for your database server + $doctrine->dbal() + ->connection('customer') + ->url(env('DATABASE_CUSTOMER_URL')->resolve()) + ->driver('pdo_mysql') + ->serverVersion('5.7') + ->charset('utf8mb4'); + + $doctrine->orm()->defaultEntityManager('default'); + $emDefault = $doctrine->orm()->entityManager('default'); + $emDefault->connection('default'); + $emDefault->mapping('Main') + ->isBundle(false) + ->type('annotation') + ->dir('%kernel.project_dir%/src/Entity/Main') + ->prefix('App\Entity\Main') + ->alias('Main'); + + $emCustomer = $doctrine->orm()->entityManager('customer'); + $emCustomer->connection('customer'); + $emCustomer->mapping('Customer') + ->isBundle(false) + ->type('annotation') + ->dir('%kernel.project_dir%/src/Entity/Customer') + ->prefix('App\Entity\Customer') + ->alias('Customer') + ; + }; In this case, you've defined two entity managers and called them ``default`` and ``customer``. The ``default`` entity manager manages entities in the @@ -193,8 +183,8 @@ for each entity manager, but you are free to define the same connection for both the connection or entity manager, the default (i.e. ``default``) is used. If you use a different name than ``default`` for the default entity manager, - you will need to redefine the default entity manager in ``prod`` environment - configuration too: + you will need to redefine the default entity manager in the ``prod`` environment + configuration and in the Doctrine migrations configuration (if you use that): .. code-block:: yaml @@ -205,6 +195,13 @@ for each entity manager, but you are free to define the same connection for both # ... + .. code-block:: yaml + + # config/packages/doctrine_migrations.yaml + doctrine_migrations: + # ... + em: 'your default entity manager name' + When working with multiple connections to create your databases: .. code-block:: terminal @@ -235,20 +232,18 @@ the default entity manager (i.e. ``default``) is returned:: // ... use Doctrine\ORM\EntityManagerInterface; + use Doctrine\Persistence\ManagerRegistry; class UserController extends AbstractController { - public function index(EntityManagerInterface $entityManager): Response + public function index(ManagerRegistry $doctrine): Response { - // These methods also return the default entity manager, but it's preferred - // to get it by injecting EntityManagerInterface in the action method - $entityManager = $this->getDoctrine()->getManager(); - $entityManager = $this->getDoctrine()->getManager('default'); - $entityManager = $this->get('doctrine.orm.default_entity_manager'); + // Both methods return the default entity manager + $entityManager = $doctrine->getManager(); + $entityManager = $doctrine->getManager('default'); - // Both of these return the "customer" entity manager - $customerEntityManager = $this->getDoctrine()->getManager('customer'); - $customerEntityManager = $this->get('doctrine.orm.customer_entity_manager'); + // This method returns instead the "customer" entity manager + $customerEntityManager = $doctrine->getManager('customer'); // ... } @@ -270,29 +265,21 @@ The same applies to repository calls:: use AcmeStoreBundle\Entity\Customer; use AcmeStoreBundle\Entity\Product; + use Doctrine\Persistence\ManagerRegistry; // ... class UserController extends AbstractController { - public function index(): Response + public function index(ManagerRegistry $doctrine): Response { - // Retrieves a repository managed by the "default" em - $products = $this->getDoctrine() - ->getRepository(Product::class) - ->findAll() - ; + // Retrieves a repository managed by the "default" entity manager + $products = $doctrine->getRepository(Product::class)->findAll(); - // Explicit way to deal with the "default" em - $products = $this->getDoctrine() - ->getRepository(Product::class, 'default') - ->findAll() - ; + // Explicit way to deal with the "default" entity manager + $products = $doctrine->getRepository(Product::class, 'default')->findAll(); - // Retrieves a repository managed by the "customer" em - $customers = $this->getDoctrine() - ->getRepository(Customer::class, 'customer') - ->findAll() - ; + // Retrieves a repository managed by the "customer" entity manager + $customers = $doctrine->getRepository(Customer::class, 'customer')->findAll(); // ... } diff --git a/doctrine/registration_form.rst b/doctrine/registration_form.rst index 841e1960512..cf530a041e0 100644 --- a/doctrine/registration_form.rst +++ b/doctrine/registration_form.rst @@ -14,7 +14,7 @@ form you must: #. :doc:`Create a form ` to ask for the registration information (you can generate this with the ``make:registration-form`` command provided by the `MakerBundle`_); #. Create :doc:`a controller ` to :ref:`process the form `; -#. :ref:`Protect some parts of your application ` so that - only registered users can access them. +#. :ref:`Protect some parts of your application ` so that + only registered users can access to them. .. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html diff --git a/doctrine/resolve_target_entity.rst b/doctrine/resolve_target_entity.rst index 9be8730ba4a..31186cc4046 100644 --- a/doctrine/resolve_target_entity.rst +++ b/doctrine/resolve_target_entity.rst @@ -46,10 +46,8 @@ A Customer entity:: use App\Model\InvoiceSubjectInterface; use Doctrine\ORM\Mapping as ORM; - /** - * @ORM\Entity - * @ORM\Table(name="customer") - */ + #[ORM\Entity] + #[ORM\Table(name: 'customer')] class Customer extends BaseCustomer implements InvoiceSubjectInterface { // In this example, any methods defined in the InvoiceSubjectInterface @@ -66,16 +64,15 @@ An Invoice entity:: /** * Represents an Invoice. - * - * @ORM\Entity - * @ORM\Table(name="invoice") */ + #[ORM\Entity] + #[ORM\Table(name: 'invoice')] class Invoice { /** - * @ORM\ManyToOne(targetEntity="App\Model\InvoiceSubjectInterface") * @var InvoiceSubjectInterface */ + #[ORM\ManyToOne(targetEntity: InvoiceSubjectInterface::class)] protected $subject; } @@ -139,15 +136,13 @@ about the replacement: // config/packages/doctrine.php use App\Entity\Customer; use App\Model\InvoiceSubjectInterface; + use Symfony\Config\DoctrineConfig; - $container->loadFromExtension('doctrine', [ - 'orm' => [ - // ... - 'resolve_target_entities' => [ - InvoiceSubjectInterface::class => Customer::class, - ], - ], - ]); + return static function (DoctrineConfig $doctrine) { + $orm = $doctrine->orm(); + // ... + $orm->resolveTargetEntity(InvoiceSubjectInterface::class, Customer::class); + }; Final Thoughts -------------- diff --git a/email.rst b/email.rst deleted file mode 100644 index 6f54812729a..00000000000 --- a/email.rst +++ /dev/null @@ -1,669 +0,0 @@ -.. index:: - single: Emails - -Swift Mailer -============ - -.. caution:: - - In Symfony 4.3, the :doc:`Mailer ` component was introduced and should - be used instead of Swift Mailer as it won't be maintained anymore as of November - 2021. - -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`_. - -Installation ------------- - -In applications using :ref:`Symfony Flex `, run this command to -install the Swift Mailer based mailer before using it: - -.. code-block:: terminal - - $ composer require symfony/swiftmailer-bundle - -If your application doesn't use Symfony Flex, follow the installation -instructions on `SwiftMailerBundle`_. - -.. _swift-mailer-configuration: - -Configuration -------------- - -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: - -.. code-block:: bash - - # .env (or override MAILER_URL in .env.local to avoid committing your changes) - - # use this to disable email delivery - MAILER_URL=null://localhost - - # use this to configure a traditional SMTP server - MAILER_URL=smtp://localhost:465?encryption=ssl&auth_mode=login&username=&password= - -.. caution:: - - 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 ``Swift_Mailer`` service. Overall, -sending an email is pretty straightforward:: - - 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/emails/registration.html.twig - 'emails/registration.html.twig', - ['name' => $name] - ), - 'text/html' - ) - - // you can remove the following code if you don't define a text version for your emails - ->addPart( - $this->renderView( - // templates/emails/registration.txt.twig - 'emails/registration.txt.twig', - ['name' => $name] - ), - 'text/plain' - ) - ; - - $mailer->send($message); - - return $this->render(...); - } - -To keep things decoupled, the email body has been stored in a template and -rendered with the ``renderView()`` method. The ``registration.html.twig`` -template might look something like this: - -.. code-block:: html+twig - - {# templates/emails/registration.html.twig #} -

You did it! You registered!

- - Hi {{ name }}! You're successfully registered. - - {# example, assuming you have a route named "login" #} - To login, go to:
.... - - Thanks! - - {# Makes an absolute URL to the /images/logo.png file #} - - -The ``$message`` object supports many more options, such as including attachments, -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`_, - `Mailtrap`_ 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 being sent. 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`` -......................................... - -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 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 -.. _`Mailtrap`: https://mailtrap.io/ -.. _`Swift Mailer`: https://swiftmailer.symfony.com/ -.. _`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`: https://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/event_dispatcher.rst b/event_dispatcher.rst index 5ac86e55cd5..8f3647c8a4d 100644 --- a/event_dispatcher.rst +++ b/event_dispatcher.rst @@ -67,12 +67,6 @@ The most common way to listen to an event is to register an **event listener**:: Check out the :doc:`Symfony events reference ` to see what type of object each event provides. -.. versionadded:: 4.3 - - The :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` class was - introduced in Symfony 4.3. In previous versions it was called - ``Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent``. - 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": @@ -106,11 +100,17 @@ using a special "tag": .. code-block:: php // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use App\EventListener\ExceptionListener; - $container->register(ExceptionListener::class) - ->tag('kernel.event_listener', ['event' => 'kernel.exception']) - ; + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(ExceptionListener::class) + ->tag('kernel.event_listener', ['event' => 'kernel.exception']) + ; + }; Symfony follows this logic to decide which method to call inside the event listener class: @@ -118,7 +118,7 @@ listener class: #. If the ``kernel.event_listener`` tag defines the ``method`` attribute, that's the name of the method to be called; #. If no ``method`` attribute is defined, try to call the method whose name - is ``on`` + "camel-cased event name" (e.g. ``onKernelException()`` method for + is ``on`` + "PascalCased event name" (e.g. ``onKernelException()`` method for the ``kernel.exception`` event); #. If that method is not defined either, try to call the ``__invoke()`` magic method (which makes event listeners invokable); @@ -134,6 +134,54 @@ listener class: internal Symfony listeners usually range from ``-256`` to ``256`` but your own listeners can use any positive or negative integer. +Defining Event Listeners with PHP Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An alternative way to define an event listener is to use the +:class:`Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener` +PHP attribute. This allows to configure the listener inside its class, without +having to add any configuration in external files:: + + namespace App\EventListener; + + use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + + #[AsEventListener] + final class MyListener + { + public function __invoke(CustomEvent $event): void + { + // ... + } + } + +You can add multiple ``#[AsEventListener()]`` attributes to configure different methods:: + + namespace App\EventListener; + + use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + + #[AsEventListener(event: CustomEvent::class, method: 'onCustomEvent')] + #[AsEventListener(event: 'foo', priority: 42)] + #[AsEventListener(event: 'bar', method: 'onBarEvent')] + final class MyMultiListener + { + public function onCustomEvent(CustomEvent $event): void + { + // ... + } + + public function onFoo(): void + { + // ... + } + + public function onBarEvent(): void + { + // ... + } + } + .. _events-subscriber: Creating an Event Subscriber @@ -206,10 +254,10 @@ the ``EventSubscriber`` directory. Symfony takes care of the rest. Request Events, Checking Types ------------------------------ -A single page can make several requests (one master request, and then multiple +A single page can make several requests (one main request, and then multiple 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":: +a "main" request or a "sub request":: // src/EventListener/RequestListener.php namespace App\EventListener; @@ -220,8 +268,8 @@ a "master" request or a "sub request":: { public function onKernelRequest(RequestEvent $event) { - if (!$event->isMasterRequest()) { - // don't do anything if it's not the master request + if (!$event->isMainRequest()) { + // don't do anything if it's not the main request return; } @@ -275,11 +323,6 @@ name (FQCN) of the corresponding event class:: } } -.. versionadded:: 4.3 - - Referring Symfony's core events via the FQCN of the event class is possible - since Symfony 4.3. - Internally, the event FQCN are treated as aliases for the original event names. Since the mapping already happens when compiling the service container, event listeners and subscribers using FQCN instead of event names will appear under @@ -310,10 +353,6 @@ The compiler pass will always extend the existing list of aliases. Because of that, it is safe to register multiple instances of the pass with different configurations. -.. versionadded:: 4.4 - - The ``AddEventAliasesPass`` class was introduced in Symfony 4.4. - Debugging Event Listeners ------------------------- @@ -331,6 +370,21 @@ its name: $ php bin/console debug:event-dispatcher kernel.exception +or can get everything which partial matches the event name: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher kernel // matches "kernel.exception", "kernel.response" etc. + $ php bin/console debug:event-dispatcher Security // matches "Symfony\Component\Security\Http\Event\CheckPassportEvent" + +The :doc:`security ` system uses an event dispatcher per +firewall. Use the ``--dispatcher`` option to get the registered listeners +for a particular event dispatcher: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher --dispatcher=security.event_dispatcher.main + Learn more ---------- diff --git a/form/bootstrap4.rst b/form/bootstrap4.rst index 31f7e50cf1a..bbcd0819369 100644 --- a/form/bootstrap4.rst +++ b/form/bootstrap4.rst @@ -55,13 +55,13 @@ configuration: .. code-block:: php // config/packages/twig.php - $container->loadFromExtension('twig', [ - 'form_themes' => [ - 'bootstrap_4_layout.html.twig', - ], + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig) { + $twig->formThemes(['bootstrap_4_layout.html.twig']); // ... - ]); + }; If you prefer to apply the Bootstrap styles on a form to form basis, include the ``form_theme`` tag in the templates where those forms are used: @@ -93,7 +93,7 @@ you'll get the error messages displayed *twice*. Since form errors are rendered *inside* the ``